From adf500a2a8383085cb91916bce6beef895d3e530 Mon Sep 17 00:00:00 2001 From: jgasper Date: Thu, 11 Jan 2018 11:14:50 -0800 Subject: [PATCH] Initial multi-purpose Grouper image (#6) * Lots of clean-up: scim/tomee working; setuid added for Tomcat and Shibd; * updated readme and other clean-up * test-compose documentation and other refinements * Added additional notes --- .dockerignore | 3 +- Dockerfile | 66 ++-- README.md | 106 ++++-- container_files/grouper.installer.properties | 24 +- container_files/httpd/grouper-www.conf | 1 + .../tier-support/grouper-ws-scim.xml | 6 + container_files/tier-support/grouper-ws.xml | 2 +- container_files/tier-support/grouper.xml | 2 +- ...isord-web.conf => supervisord-tomcat.conf} | 6 +- .../tier-support/supervisord-tomee.conf | 31 ++ container_files/{ => tomcat/conf}/server.xml | 8 +- .../tomee/conf}/server.xml | 320 ++++++++-------- container_files/usr-local-bin/gsh | 8 + container_files/usr-local-bin/library.sh | 18 +- container_files/usr-local-bin/scim | 8 + container_files/usr-local-bin/ui | 4 +- container_files/usr-local-bin/ui-ws | 4 +- container_files/usr-local-bin/ws | 2 +- manualBuild.sh | 2 +- test-compose/README.md | 63 +++- test-compose/daemon/Dockerfile | 2 +- test-compose/data/Dockerfile | 5 +- .../conf/grouper.hibernate.properties | 0 .../conf/grouper.properties | 0 .../{ => container_files}/conf/sources.xml | 0 .../seed-data/bootstrap.gsh | 0 .../seed-data/ds-setup.inf | 0 .../seed-data/sisData.sql | 0 .../seed-data/users.ldif | 0 test-compose/docker-compose.yml | 40 +- test-compose/gsh/Dockerfile | 2 +- test-compose/scim/Dockerfile | 5 + test-compose/ui/Dockerfile | 5 +- .../ui/{ => container_files}/WEB-INF/web.xml | 0 .../shibboleth/shibd.logger | 0 test-compose/ws/Dockerfile | 6 +- .../ws/{ => container_files}/WEB-INF/web.xml | 0 .../container_files}/tomcat/server.xml | 351 +++++++++--------- 38 files changed, 662 insertions(+), 438 deletions(-) create mode 100644 container_files/tier-support/grouper-ws-scim.xml rename container_files/tier-support/{supervisord-web.conf => supervisord-tomcat.conf} (90%) create mode 100644 container_files/tier-support/supervisord-tomee.conf rename container_files/{ => tomcat/conf}/server.xml (98%) rename {test-compose/ws/tomcat => container_files/tomee/conf}/server.xml (58%) create mode 100755 container_files/usr-local-bin/gsh create mode 100755 container_files/usr-local-bin/scim rename test-compose/data/{ => container_files}/conf/grouper.hibernate.properties (100%) rename test-compose/data/{ => container_files}/conf/grouper.properties (100%) rename test-compose/data/{ => container_files}/conf/sources.xml (100%) rename test-compose/data/{ => container_files}/seed-data/bootstrap.gsh (100%) rename test-compose/data/{ => container_files}/seed-data/ds-setup.inf (100%) rename test-compose/data/{ => container_files}/seed-data/sisData.sql (100%) rename test-compose/data/{ => container_files}/seed-data/users.ldif (100%) create mode 100644 test-compose/scim/Dockerfile rename test-compose/ui/{ => container_files}/WEB-INF/web.xml (100%) rename test-compose/ui/{ => container_files}/shibboleth/shibd.logger (100%) rename test-compose/ws/{ => container_files}/WEB-INF/web.xml (100%) rename test-compose/{ui => ws/container_files}/tomcat/server.xml (90%) diff --git a/.dockerignore b/.dockerignore index b66e30b..ac0479b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ .git/ test-compose/ *.md -manualBuild.sh \ No newline at end of file +manualBuild.sh +LICENSE diff --git a/Dockerfile b/Dockerfile index eeccd7b..92eef61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,11 @@ RUN yum update -y \ && yum install -y wget tar unzip dos2unix \ && yum clean all -ENV ANT_VERSION="1.10.1" \ - GROUPER_VERSION=2.3.0 \ - JAVA_HOME=/opt/openjdk8 \ - TOMCAT_MAJOR=8 \ - TOMCAT_VERSION="8.5.23" - -RUN java_version=8.0.131; \ - zulu_version=8.21.0.1; \ +ENV GROUPER_VERSION=2.3.0 \ + JAVA_HOME=/opt/java + +RUN java_version=8.0.131; \ + zulu_version=8.21.0.1; \ echo 'Downloading the OpenJDK Zulu...' \ && wget -q http://cdn.azul.com/zulu/bin/zulu$zulu_version-jdk$java_version-linux_x64.tar.gz \ && echo "1931ed3beedee0b16fb7fd37e069b162 zulu$zulu_version-jdk$java_version-linux_x64.tar.gz" | md5sum -c - \ @@ -40,30 +37,40 @@ RUN echo 'Installing Grouper'; \ cd /opt/grouper/$GROUPER_VERSION/ \ && $JAVA_HOME/bin/java -cp :grouperInstaller.jar edu.internet2.middleware.grouperInstaller.GrouperInstaller -#The Grouper Installer is corrupting the Messaging Jar files. -RUN cp /opt/grouper/2.3.0/grouper.rabbitMq-2.3.0/lib/* /opt/grouper/2.3.0/grouper.apiBinary-2.3.0/lib/grouper/ \ - && cp /opt/grouper/2.3.0/grouper.activeMq-2.3.0/lib/* /opt/grouper/2.3.0/grouper.apiBinary-2.3.0/lib/grouper/ FROM centos:centos7 as cleanup -COPY --from=installing /opt/grouper/2.3.0/grouper.apiBinary-2.3.0/ /opt/grouper/grouper.apiBinary -COPY --from=installing /opt/grouper/2.3.0/grouper.ui-2.3.0/dist/grouper/ /opt/grouper/grouper.ui/dist/grouper/ -COPY --from=installing /opt/grouper/2.3.0/grouper.ws-2.3.0/grouper-ws/build/dist/grouper-ws/ /opt/grouper/grouper.ws/dist/grouper-ws/ -COPY --from=installing /opt/grouper/2.3.0/apache-tomcat-8.5.12/ /opt/tomcat/ -COPY --from=installing /opt/grouper/2.3.0/apache-tomee-webprofile-7.0.0/ /opt/tomee/ -COPY --from=installing /opt/grouper/2.3.0/grouper.clientBinary-2.3.0/ /opt/grouper/grouper.clientBinary/ +ENV GROUPER_VERSION=2.3.0 \ + TOMCAT_VERSION=8.5.12 \ + TOMEE_VERSION=7.0.0 + +COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouperInstaller.jar /opt/grouper/ +COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouper.apiBinary-$GROUPER_VERSION/ /opt/grouper/grouper.apiBinary/ +COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouper.ui-$GROUPER_VERSION/dist/grouper/ /opt/grouper/grouper.ui/ +COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouper.ws-$GROUPER_VERSION/grouper-ws/build/dist/grouper-ws/ /opt/grouper/grouper.ws/ +COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouper.ws-$GROUPER_VERSION/grouper-ws-scim/targetBuiltin/grouper-ws-scim/ /opt/grouper/grouper.scim/ +#COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouper.clientBinary-$GROUPER_VERSION/ /opt/grouper/grouper.clientBinary/ +COPY --from=installing /opt/grouper/$GROUPER_VERSION/apache-tomcat-$TOMCAT_VERSION/ /opt/tomcat/ +COPY --from=installing /opt/grouper/$GROUPER_VERSION/apache-tomee-webprofile-$TOMEE_VERSION/ /opt/tomee/ RUN cd /opt/grouper/grouper.apiBinary/; \ - rm -fr ddlScripts/ grouper.lck grouper.log grouper.script grouper.tmp/ gshAddGrouperSystemWsGroup.gsh logs/ + rm -fr ddlScripts/ grouper.lck grouper.log grouper.script grouper.tmp/ gshAddGrouperSystemWsGroup.gsh logs/ + +RUN cd /opt/tomcat/; \ + rm -fr webapps/docs/ webapps/examples/ webapps/host-manager/ webapps/manager/ logs/* temp/* work/* \ + && mkdir -p logs/grouperUi logs/grouperWs -RUN cd /opt/tomcat/webapps/; \ - rm -fr docs/ examples/ host-manager/ manager/ logs/* +RUN cd /opt/tomee/; \ + rm -fr webapps/docs/ webapps/host-manager/ webapps/manager/ logs/* temp/* work/* -RUN cd /opt/tomee/webapps/; \ - rm -fr docs/ host-manager/ manager/ +RUN sed -i "s/\/opt\/grouper\/$GROUPER_VERSION\/apache-tomcat-$TOMCAT_VERSION/\/opt\/tomcat/g" /opt/grouper/grouper.ui/WEB-INF/classes/log4j.properties \ + && sed -i "s/\/opt\/grouper\/$GROUPER_VERSION\/apache-tomcat-$TOMCAT_VERSION/\/opt\/tomcat/g" /opt/grouper/grouper.ws/WEB-INF/classes/log4j.properties \ + && sed -i 's/${grouper.home}/\/opt\/tomee\//g' /opt/grouper/grouper.scim/WEB-INF/classes/log4j.properties + +COPY container_files/tomcat/ /opt/tomcat/ +COPY container_files/tomee/ /opt/tomee/ -COPY container_files/server.xml /opt/tomcat/conf/ FROM tier/shibboleth_sp @@ -74,8 +81,9 @@ LABEL author="tier-packaging@internet2.edu " \ ImageName=$imagename \ ImageOS=centos7 -ENV JAVA_HOME=/opt/openjdk8 \ - PATH=$PATH:$JAVA_HOME/bin +ENV JAVA_HOME=/opt/java \ + PATH=$PATH:$JAVA_HOME/bin \ + GROUPER_HOME=/opt/grouper/grouper.apiBinary RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime @@ -84,12 +92,18 @@ RUN yum update -y \ && pip install --upgrade pip \ && pip install supervisor \ && yum clean -y all - + COPY --from=installing $JAVA_HOME $JAVA_HOME COPY --from=cleanup /opt/tomcat/ /opt/tomcat/ COPY --from=cleanup /opt/tomee/ /opt/tomee/ COPY --from=cleanup /opt/grouper/ /opt/grouper/ +RUN groupadd -r tomcat \ + && useradd -r -m -s /sbin/nologin -g tomcat tomcat \ + && mkdir -p /opt/tomcat/logs/ /opt/tomcat/temp/ /opt/tomcat/work/ \ + && chown -R tomcat:tomcat /opt/tomcat/logs/ /opt/tomcat/temp/ /opt/tomcat/work/ \ + && chown -R tomcat:tomcat /opt/tomee/logs/ /opt/tomee/temp/ /opt/tomee/work/ + COPY container_files/tier-support/ /opt/tier-support/ COPY container_files/usr-local-bin /usr/local/bin/ COPY container_files/httpd/* /etc/httpd/conf.d/ diff --git a/README.md b/README.md index 0dc1cdb..78470a7 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,12 @@ Grouper is an enterprise access management system designed for the highly distri ![logo](https://www.internet2.edu/media/medialibrary/2013/10/15/image_grouper_logowordmark_bw.png) # How to use this image -This image provides support for each of the Grouper components/roles: Grouper Daemon/Loader, Grouper UI, and Grouper Web Services. + +This image provides support for each of the Grouper components/roles: Grouper Daemon/Loader, Grouper UI, Grouper Web Services, and Grouper SCIM Server. ## Starting each role -While TIER recommends/supports using Docker Swarm for orchestrating the Grouper environment, these containers can be run directly. Both examples are shown below. It should be noted that these examples will not run independently, but required additional configuration components to be provided before each container will start as expected. +While TIER recommends/supports using Docker Swarm for orchestrating the Grouper environment, these containers can be run directly (or with other orchestration products). Both examples are shown below. It should be noted that these examples will not run independently, but required additional configuration to be provided before each container will start as expected. ### Daemon/Loader @@ -46,6 +47,20 @@ Run the Grouper Daemon/Loader as a standalone container. $ docker run --detach --name grouper-daemon tier/grouper:latest daemon ``` +### SCIM Server + +Runs the Grouper SCIM Server as a service. + +```console +$ docker service create --detach --publish 9443:443 --name grouper-ws tier/grouper:latest scim +``` + +Runs the Grouper Web Services in a standalone container. + +```console +$ docker run --detach --publish 9443:443 --name grouper-daemon tier/grouper:latest scim +``` + ### UI Runs the Grouper UI as a service. @@ -76,7 +91,7 @@ $ docker run --detach --publish 8443:443 --name grouper-daemon tier/grouper:late ### UI and Web Services -This good when first starting to work with Grouper, but when scaling Grouper UI or Web Services it is advisable to use the individual roles noted above. +> This method is good when first starting to work with Grouper, but when scaling Grouper UI or Web Services it is advisable to use the individual roles noted above. Runs the Grouper UI and Web Services as a combined service. (You should really run these as individual roles to take advantage of Docker service replicas.) @@ -101,10 +116,14 @@ $ docker run -it --rm tier/grouper:latest bin/gsh # Configuration ## Grouper Configurations + There are several things that are required for this image to successfully start. At a minimum, the `grouper.hibernate.properties` and `subject.properties` (or the old `sources.xml` equivalent) files need to be customized and available to the container at start-up. +Grouper config files maybe placed into `/opt/grouper/conf` and these files will be put into the appropriate location based on the role the container assumes. Docker Secrets starting with the name `grouper_` should take precedence over these files. (See below.) + ## Web Apps Configuration -If starting the container to serve the Grouper UI or Grouper Web Services components, a TLS key and cert(s) need to be applied to those containers. + +If starting the container to serve the Grouper UI, Grouper Web Services, Grouper SCIM Server components, a TLS key and cert(s) need to be applied to those containers. The Grouper UI also requires some basic Shibboleth SP configuration. The `/etc/shibboleth/shibboleth2.xml` file should be modified to set: - an entityId for the SP @@ -112,82 +131,107 @@ The Grouper UI also requires some basic Shibboleth SP configuration. The `/etc/s - set the SP's encryption keys - the identity attribute of the subject to be passed to Grouper -If encrpytion keys are defined in the `shibboleth2.xml` file, then the key/cert should be provided as well. The `attribute-map.xml` file has most of the common identity attributes pre-configured, but it (and other Shibbolrth SP files) can be overlaid/replaced as necessary. +If encryption keys are defined in the `shibboleth2.xml` file, then the key/cert files should be provided as well. The `attribute-map.xml` file has most of the common identity attributes pre-configured, but it (and other Shibboleth SP files) can be overlaid/replaced as necessary. ## General Configuration Mechanism -There are three primary ways to provide the Grouper and additional configuration to the container: Docker Config/Secrets, customized images, and bind mounts. Depending upon your needs you may use a combination of two or three of these options. + +There are three primary ways to provide Grouper and additional configuration files to the container: Docker Config/Secrets, customized images, and bind mounts. Depending upon your needs you may use a combination of two or three of these options. ### Secrets/Configs -Docker Config and Docker Secrets is Docker's way of providing configurations files to a container. The primary difference between the Config and Secrets functionality is that Secrets is designed to protect resrouces that sensitive files. +Docker Config and Docker Secrets are Docker's way of providing configurations files to a container at runtime. The primary difference between the Config and Secrets functionality is that Secrets is designed to protect resources/files that are sensitive. -This image will make any secrets (containing a period in the secret name) available to the appropriate Grouper component's conf directory (i.e. `/conf` or `WEB-INF/classes`). These file will supercede any in the underlying image. +This container will make any secrets with secret names prepended with `grouper_` available to the appropriate Grouper component's conf directory (i.e. `/conf` or `WEB-INF/classes`). Any secrets with secret names starting with `shib_` will be available in the Shibboleth SP `/etc/shibboleth/` directory. Any secrets with secret names starting with `httpd_` will be available to `/etc/httpd/conf.d` directory. Finally, if a secret with the name of `host-key.pem` will be mapped to the httpd TLS cert used by Grouper UI, Grouper WS, and Grouper SCIM Server containers. These files will supercede any found in the underlying image. -Secrets can be managed using the `docker secret` command: `docker secret create grouper.hibernate.properties ./grouper.hibernate.properties`. This will securely store the file in the swarm. Secrets can then be assigned to the service `docker service create -d --name daemon --secret grouper.hibernate.properties --secret sources.xml tier/grouper daemon`. +Secrets can be managed using the `docker secret` command: `docker secret create grouper_grouper.hibernate.properties ./grouper.hibernate.properties`. This will securely store the file in the swarm. Secrets can then be assigned to the service `docker service create -d --name daemon --secret grouper_grouper.hibernate.properties --secret grouper_sources.xml tier/grouper daemon`. > `docker run` does not support secrets; Bind mounts need to be used instead. ### Bind Mounts -Bind mounts can be used to connect files on the Docker host into the container. When not running in swarm mode, the secrets are supported, so we can use a bind mount to provide the container with the configuration files. +Bind mounts can be used to connect files/folders on the Docker host into the container's file system. Unless running in swarm mode, the secrets are not supported, so we can use a bind mount to provide the container with the configuration files. -``` -docker run --detach --name daemon \ - --mount type=bind,src=$(pwd)/grouper.hibernate.properties,dst=/run/secrets/grouper.hibernate.properties \ - --mount type=bind,src=$(pwd)/sources.xml,dst=/run/secrets/sources.xml \ +```console +$ docker run --detach --name daemon \ + --mount type=bind,src=$(pwd)/grouper.hibernate.properties,dst=/run/secrets/grouper_grouper.hibernate.properties \ + --mount type=bind,src=$(pwd)/sources.xml,dst=/run/secrets/grouper_sources.xml \ tier/grouper daemon ``` ### Customized Images -Deployers will undoubtedly want to add in their files to the container; things like addtional jar files defining Grouper Hooks, or things like own images, css files, anything. This can be accomplished by building custom images. **Deployers should NOT use this method to store sensitive configuration files.** +Deployers will undoubtedly want to add in their files to the container. Things like additional jar files defining Grouper Hooks, or things like images and css files. This can be accomplished by building custom images. **Deployers should NOT use this method to store sensitive configuration files.** To add a favicon to the Grouper UI, we use the tier/grouper images as a base and `COPY` our local `favicon.ico` into the image. While we are at it, we define this image as a UI image by specifying the default commnd (i.e `CMD`) of `ui`. ```Dockerfile FROM tier/grouper:latest -COPY favicon.ico /opt/grouper/grouper.ui/dist/grouper/ +COPY favicon.ico /opt/grouper/grouper.ui/ CMD ui ``` To build our image: -``` -docker build --tag=org/grouper-ui . +```console +$ docker build --tag=org/grouper-ui . ``` This image can now be used locally or pushed to an organization's Docker repository. -## Memory Limits -(TODO) +## Environment Variables + +Deployers can set runtime variables to both the Grouper Shell and Loader/Daemon and to Tomcat/Tomcat EE using environment variables. These can be set using the `docker run` and `docker service creates`'s `--env` paramater. +### Grouper Shell/Loader + +The following environment variables are used by the Grouper Shell/Loader: +- MEM_START: corresponds to the java's `-Xms`. +- MEM_MAX: corresponds to java's `-Xmx`. + +### Tomcat/TomEE + +Amongst others variables defined in the `catalina.sh`, the following variables would like be useful for deployers: +- CATALINA_OPTS: Java runtime options to only be used by Tomcat itself. # File System Endpoints -Significant directories and files that deployers should be aware of. -- `/grouper/conf/`: a common directory to place non-sensitive config files that will be placed into the appropriate location for each Grouper component at container start-up. -- `/grouper/lib/`: a common directory to place additional jar files that will be placed into the appropriate location for each Grouper component at container start-up. +Here is a list of significant directories and files that deployers should be aware of. +- `/opt/grouper/conf/`: a common directory to place non-sensitive config files that will be placed into the appropriate location for each Grouper component at container start-up. +- `/opt/grouper/lib/`: a common directory to place additional jar files that will be placed into the appropriate location for each Grouper component at container start-up. +- `/opt/grouper/grouper.apiBinary/`: location to overlay Grouper GSH or Daemon/Loader files. +`/opt/grouper/grouper.scim/`: location for overlaying Grouper SCIM Server web application files (expanded `grouper-ws-scim.war`). +- `/opt/grouper/grouper.ui/`: location for overlaying Grouper UI web application files (expanded `grouper.war`). +- `/opt/grouper/grouper.ws/`: location for overlaying Grouper Web Services web application files (expanded `grouper-ws.war`). - `/etc/httpd/conf.d/ssl-enabled.conf`: Can be overlaid to change the TLS settings when running Grouper UI or Web Servicse. - `/etc/shibboleth/`: location to overlay the Shibboleth SP configuration files used by the image. -- `/opt/grouper/grouper.apiBinary/`: location to overlay Grouper GSH or Daemon/Loader files. -- `/opt/grouper/grouper.ui/dist/grouper/`: location to overlay Grouper UI web application files. -- `/opt/grouper/grouper.ws/dist/grouper-ws/`: location to overlay Grouper Web Services web application files. +- `/opt/tomcat/`: used to run Grouper UI and Grouper WS +- `/opt/tomee/`: used to run the Grouper SCIM Server. +- `/var/run/secrets`: location where Docker Secrets are mounted into the container. Secrets starting with `grouper_`, `shib_`, and `httpd_` have special meaning. See `Secrets/Configs` above. -To examine baseline image files, one might run `docker run --name=temp -it tier/grouper bash` and browse through these endpoints. While the container is running one may copy files out of the image/container using something like `docker cp temp:/opt/grouper/grouper.api/conf/grouper.properties .`, which will copy the `grouper.properties` to the host's present working directory. These files can then be edited and applied via the mechanisms outlined above. +To examine baseline image files, one might run `docker run --name=temp -it tier/grouper bash` and browse through these file system endpoints. While the container is running one may copy files out of the image/container using something like `docker cp containerId:/opt/grouper/grouper.api/conf/grouper.properties .`, which will copy the `grouper.properties` to the Docker client's present working directory. These files can then be edited and applied via the mechanisms outlined above. # Provisioning a Grouper Database -(TODO) -``` -docker run --detach --rm --name daemon \ - --mount type=bind,src=$(pwd)/grouper.hibernate.properties,dst=/run/secrets/grouper.hibernate.properties \ +Using standard methods, create a MariaDb Server and an empty Grouper database. Create a database user with privileges to create and populate schema objects. Set the appropriate database connection properties in `grouper.hibernate.properties`. Be sure to the user created with schema manipulation privileges as the db user. + +Next populate the database by using the following command. + +```console +$ docker run -it --rm \ + --mount type=bind,src=$(pwd)/grouper.hibernate.properties,dst=/run/secrets/grouper_grouper.hibernate.properties \ tier/grouper gsh -registry -check -runscript -noprompt ``` +Note: a less privileged database user maybe used when running the typical Grouper roles. This user need SELECT, INSERT, UPDATE, and DELETE privileges on the schema objects. + +# Misc Notes + +- [HTTP Strict Transport Security (HSTS)](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) is enabled on the Apache HTTP Server. +- morphStrings functionality in Grouper is supported. It is recommended that the various morphString files be associated with the containers as Docker Secrets. Set the configuration file properties to use `/var/run/secrets/secretname`. # License diff --git a/container_files/grouper.installer.properties b/container_files/grouper.installer.properties index 303da95..c8f4095 100644 --- a/container_files/grouper.installer.properties +++ b/container_files/grouper.installer.properties @@ -6,10 +6,7 @@ grouper.version = 2.3.0 grouperInstaller.print.autorunKeys = true # default to install or upgrade (default is install) grouperInstaller.default.installOrUpgrade = install -# where to get grouper source from, the variable $BRANCH_NAME$ will be substituted for the branch -download.source.url = https://github.com/Internet2/grouper/archive/$BRANCH_NAME$.zip -# where to get grouper psp source from, the variable $BRANCH_NAME$ will be substituted for the branch -download.pspSource.url = https://github.com/Internet2/grouper-psp/archive/$BRANCH_NAME$.zip + ############################## ## Autorun properties ## @@ -23,23 +20,10 @@ grouperInstaller.autorun.useDefaultsAsMuchAsAvailable = true ## Note: not all of them need to be filled out for all operations # autorun grouper system password (its not secure to have a plain text pass in a config file) grouperInstaller.autorun.grouperSystemPassword = XXXXXXXXXX -# autorun Enter the database URL -grouperInstaller.autorun.dbUrl = jdbc:mysql://localhost:3306/grouper -# autorun database user -grouperInstaller.autorun.dbUser = grouper -# autorun database pass (note, it is not good security to have plaintext passwords in text config files) -grouperInstaller.autorun.dbPass = XXXXXXXXXX -# autorun Do you want to init the database (delete all existing grouper tables, add new ones) (t|f)? -grouperInstaller.autorun.deleteAndInitDatabase = t - -# [jvf] The 'fake' run-through of -# While running the installer, we don't have access to the database -#grouperInstaller.autorun.deleteAndInitDatabase = f -# While running the installer, we don't have access to the database -#grouperInstaller.autorun.addQuickstartSubjectsToDb = f - -# grouperInstaller.autorun.addQuickstartData = f +grouperInstaller.autorun.deleteAndInitDatabase = t +grouperInstaller.autorun.addQuickstartData = f +grouperInstaller.autorun.installClient = f grouperInstaller.autorun.installGrouperActiveMqMessaging = t grouperInstaller.autorun.activeMqWhereInstalled = /opt/grouper/2.3.0/grouper.apiBinary-2.3.0/ diff --git a/container_files/httpd/grouper-www.conf b/container_files/httpd/grouper-www.conf index 6d15d22..368f7f6 100644 --- a/container_files/httpd/grouper-www.conf +++ b/container_files/httpd/grouper-www.conf @@ -5,6 +5,7 @@ ProxyBadHeader Ignore ProxyPass /grouper ajp://localhost:8009/grouper timeout=2400 ProxyPass /grouper-ws ajp://localhost:8009/grouper-ws timeout=2400 +ProxyPass /grouper-ws-scim ajp://localhost:8009/grouper-ws-scim timeout=2400 AuthType shibboleth diff --git a/container_files/tier-support/grouper-ws-scim.xml b/container_files/tier-support/grouper-ws-scim.xml new file mode 100644 index 0000000..bb15b17 --- /dev/null +++ b/container_files/tier-support/grouper-ws-scim.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/container_files/tier-support/grouper-ws.xml b/container_files/tier-support/grouper-ws.xml index e4812a4..b9aa647 100644 --- a/container_files/tier-support/grouper-ws.xml +++ b/container_files/tier-support/grouper-ws.xml @@ -1,4 +1,4 @@ - + diff --git a/container_files/tier-support/grouper.xml b/container_files/tier-support/grouper.xml index d9c6134..22cfbd8 100644 --- a/container_files/tier-support/grouper.xml +++ b/container_files/tier-support/grouper.xml @@ -1,4 +1,4 @@ - + diff --git a/container_files/tier-support/supervisord-web.conf b/container_files/tier-support/supervisord-tomcat.conf similarity index 90% rename from container_files/tier-support/supervisord-web.conf rename to container_files/tier-support/supervisord-tomcat.conf index 34ace41..3bb7553 100644 --- a/container_files/tier-support/supervisord-web.conf +++ b/container_files/tier-support/supervisord-tomcat.conf @@ -3,7 +3,7 @@ logfile=/dev/fd/1 ; supervisord log file logfile_maxbytes=0 ; maximum size of logfile before rotation loglevel=error ; info, debug, warn, trace nodaemon=true ; run supervisord as a daemon -;user=root ; default user +user=root ; default user [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface @@ -22,13 +22,15 @@ stdout_logfile = /dev/fd/1 stdout_logfile_maxbytes=0 [program:shibbolethsp] -command=/usr/sbin/shibd -f +user=shibd +command=/usr/sbin/shibd -f -F stderr_logfile = /dev/fd/2 stderr_logfile_maxbytes=0 stdout_logfile = /dev/fd/1 stdout_logfile_maxbytes=0 [program:tomcat] +user=tomcat command=/opt/tomcat/bin/catalina.sh run stderr_logfile = /dev/fd/2 stderr_logfile_maxbytes=0 diff --git a/container_files/tier-support/supervisord-tomee.conf b/container_files/tier-support/supervisord-tomee.conf new file mode 100644 index 0000000..40d0c81 --- /dev/null +++ b/container_files/tier-support/supervisord-tomee.conf @@ -0,0 +1,31 @@ +[supervisord] +logfile=/dev/fd/1 ; supervisord log file +logfile_maxbytes=0 ; maximum size of logfile before rotation +loglevel=error ; info, debug, warn, trace +nodaemon=true ; run supervisord as a daemon +user=root ; default user + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket + +; Our processes +; writing output to stdout (1) and err (2) (for Docker logging) and disabling log rotation + +[program:httpd] +command=httpd -DFOREGROUND +stderr_logfile = /dev/fd/2 +stderr_logfile_maxbytes=0 +stdout_logfile = /dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:tomee] +user=tomcat +command=/opt/tomee/bin/catalina.sh run +stderr_logfile = /dev/fd/2 +stderr_logfile_maxbytes=0 +stdout_logfile = /dev/fd/1 +stdout_logfile_maxbytes=0 + diff --git a/container_files/server.xml b/container_files/tomcat/conf/server.xml similarity index 98% rename from container_files/server.xml rename to container_files/tomcat/conf/server.xml index 99523f8..d07f66f 100644 --- a/container_files/server.xml +++ b/container_files/tomcat/conf/server.xml @@ -38,11 +38,13 @@ + - + + - \ No newline at end of file + diff --git a/test-compose/ws/tomcat/server.xml b/container_files/tomee/conf/server.xml similarity index 58% rename from test-compose/ws/tomcat/server.xml rename to container_files/tomee/conf/server.xml index 29e4905..e5c8996 100644 --- a/test-compose/ws/tomcat/server.xml +++ b/container_files/tomee/conf/server.xml @@ -1,156 +1,164 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/container_files/usr-local-bin/gsh b/container_files/usr-local-bin/gsh new file mode 100755 index 0000000..e68f947 --- /dev/null +++ b/container_files/usr-local-bin/gsh @@ -0,0 +1,8 @@ +#!/bin/bash +set -x + +. /usr/local/bin/library.sh + +prepDaemon + +exec bin/gsh "$@" diff --git a/container_files/usr-local-bin/library.sh b/container_files/usr-local-bin/library.sh index fcf92cc..d8b1dd9 100644 --- a/container_files/usr-local-bin/library.sh +++ b/container_files/usr-local-bin/library.sh @@ -29,8 +29,22 @@ prepDaemon() { fi } +prepSCIM() { + local dest=/opt/grouper/grouper.scim/WEB-INF + linkGrouperSecrets $dest/classes + + if [ -d "/opt/grouper/conf" ]; then + cp /opt/grouper/conf/* $dest/classes/ + fi + if [ -d "/opt/grouper/lib" ]; then + cp /opt/grouper/lib/* $dest/lib/ + fi + + cp /opt/tier-support/grouper-ws-scim.xml /opt/tomee/conf/Catalina/localhost/ +} + prepUI() { - local dest=/opt/grouper/grouper.ui/dist/grouper/WEB-INF + local dest=/opt/grouper/grouper.ui/WEB-INF linkGrouperSecrets $dest/classes if [ -d "/opt/grouper/conf" ]; then @@ -44,7 +58,7 @@ prepUI() { } prepWS() { - local dest=/opt/grouper/grouper.ws/dist/grouper-ws/WEB-INF + local dest=/opt/grouper/grouper.ws/WEB-INF linkGrouperSecrets $dest/classes if [ -d "/opt/grouper/conf" ]; then diff --git a/container_files/usr-local-bin/scim b/container_files/usr-local-bin/scim new file mode 100755 index 0000000..69ded13 --- /dev/null +++ b/container_files/usr-local-bin/scim @@ -0,0 +1,8 @@ +#!/bin/bash +set -x + +. /usr/local/bin/library.sh + +prepSCIM + +exec /usr/bin/supervisord -c /opt/tier-support/supervisord-tomee.conf diff --git a/container_files/usr-local-bin/ui b/container_files/usr-local-bin/ui index a451c8b..efc1699 100755 --- a/container_files/usr-local-bin/ui +++ b/container_files/usr-local-bin/ui @@ -5,4 +5,6 @@ set -x prepUI -exec /usr/bin/supervisord -c /opt/tier-support/supervisord-web.conf +export LD_LIBRARY_PATH=/opt/shibboleth/lib64:$LD_LIBRARY_PATH + +exec /usr/bin/supervisord -c /opt/tier-support/supervisord-tomcat.conf diff --git a/container_files/usr-local-bin/ui-ws b/container_files/usr-local-bin/ui-ws index 46e0471..ae19afd 100755 --- a/container_files/usr-local-bin/ui-ws +++ b/container_files/usr-local-bin/ui-ws @@ -6,4 +6,6 @@ set -x prepUI prepWS -/usr/bin/supervisord -c /opt/tier-support/supervisord-web.conf +export LD_LIBRARY_PATH=/opt/shibboleth/lib64:$LD_LIBRARY_PATH + +/usr/bin/supervisord -c /opt/tier-support/supervisord-tomcat.conf diff --git a/container_files/usr-local-bin/ws b/container_files/usr-local-bin/ws index e748390..a0a21c7 100755 --- a/container_files/usr-local-bin/ws +++ b/container_files/usr-local-bin/ws @@ -5,4 +5,4 @@ set -x prepWS -exec /usr/bin/supervisord -c /opt/tier-support/supervisord-web.conf +exec /usr/bin/supervisord -c /opt/tier-support/supervisord-tomcat.conf diff --git a/manualBuild.sh b/manualBuild.sh index 72b161b..67b7d8a 100755 --- a/manualBuild.sh +++ b/manualBuild.sh @@ -1,4 +1,4 @@ -&& docker build --pull --tag=tier/grouper . \ +docker build --pull --tag=tier/grouper:latest . \ if [[ "$OSTYPE" == "darwin"* ]]; then say build complete diff --git a/test-compose/README.md b/test-compose/README.md index 640956b..8145494 100644 --- a/test-compose/README.md +++ b/test-compose/README.md @@ -1,13 +1,62 @@ -Coming soon... +The `test-compose` directory contains an example Grouper environment that starts up the various Grouper components. This example demonstrates how one might go about customizing and deploying their Grouper containers, using the TIER Grouper image as a base image. -> This docker-stack.yml file uses the `configs` syntax which is part of the Compose file format v3.3 and requires Docker Engine version 17.06.0+ (released on 2017-06-28). Users of older engine versions will need convert `config` references to use bind mounts. After this change, everything else should work as expected. +In this example, the following cases are covered by this example: + +- A demo directory and SIS database are included, populated with approximately 1,000 test subjects. +- Grouper is configured to use this directory as the subject source. +- Grouper Loader creates groups based on the data in the SIS table. +- Grouper UI is protected by a Shibboleth IdP (included) that connects to this directory server. +- Grouper WS is protected by http basic auth that authenticates against the directory server. +- Grouper publishes event data to a RabbitMQ instance (included). + +It should be noted that while this example uses Docker Compose as a build and deployment vehicle, ideally one should use a CI server to build and publish institution specific images to an image repository as changes to the institution's customizations are committed to the source repository. These images would then be deployed to Docker Swarm, assuming that the appropriate Docker Secrets and Configs have been published to the swarm. + +# Getting Started + +From `test-compose` directory, run: + +```console +$ docker-compose up -d +``` + +This will build each of our customized images after downloading the TIER Grouper image. It will create containers for each of our components using the configuation specified in the `docker-compose.yml` file. + +To stop the Grouper environment, run: + +```console +$ docker-compose down +``` -> `configs` are not supported by docker-compose, so those are shown in the file as bind mount volumes. +When doing iterative work, such as testing UI changes or configuration changes, I find if handy to use the following command: -> Environment specific settings are passed in via secrets and configs, but anything that would standard across dev, qa, prod (e.g. jars, images, css, mods) is baked into our image. +```console +$ docker-compose kill; docker-compose rm -f; docker-compose build && docker-compose up +``` -> The files in the `data` image's `conf` directory is used to build the sample grouper database and ldap store. It is not used when the container is instantiated. +This command will clear out any remaining containers, as defined by the `docker-compose.yml` file, from the Docker host, rebuild our custom images, and start new instances of them. Because we do not specify the `-d` on the `up` command, the containers will not be forked causing the container logs to be displayed to the console, and the command prompt will not return until hitting `Ctrl+C`, which will kill the running containers. -> Rabbit MQ: guest/guest add queue `sampleQueue` to see grouper messages. +# Testing Endpoints -> In this example we don't care about the IdP secrets. They are baked into the overlay. +The components can be accessed at the following urls, with + +Grouper UI: https://localhost/grouper (username: banderson, password: password) +Grouper WS: https://localhost:8443/grouper-ws/status?diagnosticType=all +RabbmitMQ: http://localhost:15672/ (username: guest, password: guest) +MariaDB: Port 3306 (username: root, password: (no password) ) +389-ds Directory: Port 389 (username: cn=Directory Manager, password: password) + +Note that when accessing the Grouper UI, Grouper WS, or Shibboleth IdP, your browser will prompt you about an untrusted certificate. It is OK to ignore the warning while working with this example configuration. + +# Additional Notes + +- Docker `configs` are not supported by Docker Compose, so those are represented in the `docker-compose.yml` file as bind mount volumes. +- The Grouper config files in the `data` image's `conf` directory are used to build the sample grouper database and ldap store. They are not used when the container is instantiated. +- The containers will use Docker Secrets and bind mounts for non-sensitive files that are read from the `configs-ans-secrets` directory in the `test-compose` directory. +- With regard to RabbitMQ, the deployer must manually add a queue named `sampleQueue` to see Grouper messages in RabbitMQ. Messages will be dropped by RabbitMQ until this occurs. +- In this example, we don't care about the IdP secrets. They are baked into the overlay instead of using Docker Secrets. (This is not best practice for an IdP configuration, but that isn't the focus of this example.) + +# Future TODOs + +- Add a Docker Stack example + +> This docker-stack.yml file uses the `configs` syntax which is part of the Compose file format v3.3 and requires Docker Engine version 17.06.0+ (released on 2017-06-28). Users of older engine versions will need convert `config` references to use bind mounts. After this change, everything else should work as expected. diff --git a/test-compose/daemon/Dockerfile b/test-compose/daemon/Dockerfile index 5749d02..f620350 100644 --- a/test-compose/daemon/Dockerfile +++ b/test-compose/daemon/Dockerfile @@ -2,4 +2,4 @@ FROM tier/grouper:latest LABEL author="tier-packaging@internet2.edu " -CMD ["bin/gsh", "-loader"] \ No newline at end of file +CMD ["daemon"] diff --git a/test-compose/data/Dockerfile b/test-compose/data/Dockerfile index 9c88903..7739de3 100644 --- a/test-compose/data/Dockerfile +++ b/test-compose/data/Dockerfile @@ -2,10 +2,11 @@ FROM tier/grouper:latest LABEL author="tier-packaging@internet2.edu " -COPY seed-data/ /seed-data/ -COPY conf/ /opt/grouper/grouper.apiBinary/conf/ +COPY container_files/seed-data/ /seed-data/ +COPY container_files/conf/ /opt/grouper/grouper.apiBinary/conf/ RUN yum install -y epel-release \ + && yum update -y \ && yum install -y 389-ds-base 389-admin 389-adminutil mariadb-server mariadb \ && yum clean all diff --git a/test-compose/data/conf/grouper.hibernate.properties b/test-compose/data/container_files/conf/grouper.hibernate.properties similarity index 100% rename from test-compose/data/conf/grouper.hibernate.properties rename to test-compose/data/container_files/conf/grouper.hibernate.properties diff --git a/test-compose/data/conf/grouper.properties b/test-compose/data/container_files/conf/grouper.properties similarity index 100% rename from test-compose/data/conf/grouper.properties rename to test-compose/data/container_files/conf/grouper.properties diff --git a/test-compose/data/conf/sources.xml b/test-compose/data/container_files/conf/sources.xml similarity index 100% rename from test-compose/data/conf/sources.xml rename to test-compose/data/container_files/conf/sources.xml diff --git a/test-compose/data/seed-data/bootstrap.gsh b/test-compose/data/container_files/seed-data/bootstrap.gsh similarity index 100% rename from test-compose/data/seed-data/bootstrap.gsh rename to test-compose/data/container_files/seed-data/bootstrap.gsh diff --git a/test-compose/data/seed-data/ds-setup.inf b/test-compose/data/container_files/seed-data/ds-setup.inf similarity index 100% rename from test-compose/data/seed-data/ds-setup.inf rename to test-compose/data/container_files/seed-data/ds-setup.inf diff --git a/test-compose/data/seed-data/sisData.sql b/test-compose/data/container_files/seed-data/sisData.sql similarity index 100% rename from test-compose/data/seed-data/sisData.sql rename to test-compose/data/container_files/seed-data/sisData.sql diff --git a/test-compose/data/seed-data/users.ldif b/test-compose/data/container_files/seed-data/users.ldif similarity index 100% rename from test-compose/data/seed-data/users.ldif rename to test-compose/data/container_files/seed-data/users.ldif diff --git a/test-compose/docker-compose.yml b/test-compose/docker-compose.yml index 038cf48..6b2f765 100644 --- a/test-compose/docker-compose.yml +++ b/test-compose/docker-compose.yml @@ -3,7 +3,7 @@ version: "3.3" services: daemon: build: ./daemon/ - command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; daemon" + command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; exec daemon" volumes: - type: bind source: ./configs-and-secrets/grouper/grouper.properties @@ -27,7 +27,7 @@ services: ui: build: ./ui/ - command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; while ! curl -s ldap://data:389 > /dev/null; do echo waiting for ldap to start; sleep 3; done; ui" + command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; while ! curl -s ldap://data:389 > /dev/null; do echo waiting for ldap to start; sleep 3; done; exec ui" volumes: - type: bind source: ./configs-and-secrets/grouper/grouper.properties @@ -74,7 +74,7 @@ services: ws: build: ./ws/ - command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; while ! curl -s ldap://data:389 > /dev/null; do echo waiting for ldap to start; sleep 3; done; ws" + command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; while ! curl -s ldap://data:389 > /dev/null; do echo waiting for ldap to start; sleep 3; done; exec ws" volumes: - type: bind source: ./configs-and-secrets/grouper/grouper.properties @@ -106,6 +106,40 @@ services: target: grouper_sources.xml - source: host-key.pem + scim: + build: ./scim/ + command: bash -c "while ! curl -s data:3306 > /dev/null; do echo waiting for mysql to start; sleep 3; done; while ! curl -s ldap://data:389 > /dev/null; do echo waiting for ldap to start; sleep 3; done; exec scim" + volumes: + - type: bind + source: ./configs-and-secrets/grouper/grouper.properties + target: /opt/grouper/conf/grouper.properties + - type: bind + source: ./configs-and-secrets/grouper/grouper.client.properties + target: /opt/grouper/conf/grouper.client.properties + - type: bind + source: ./configs-and-secrets/httpd/host-cert.pem + target: /etc/pki/tls/certs/host-cert.pem + - type: bind + source: ./configs-and-secrets/httpd/host-cert.pem + target: /etc/pki/tls/certs/cachain.pem + depends_on: + - data + networks: + - front + - back + ports: + - "9443:443" + secrets: + - source: grouper.hibernate.properties + target: grouper_grouper.hibernate.properties + - source: grouper-loader.properties + target: grouper_grouper-loader.properties + - source: ldap.properties + target: grouper_ldap.properties + - source: sources.xml + target: grouper_sources.xml + - source: host-key.pem + gsh: build: ./gsh/ volumes: diff --git a/test-compose/gsh/Dockerfile b/test-compose/gsh/Dockerfile index 39421b2..3302328 100644 --- a/test-compose/gsh/Dockerfile +++ b/test-compose/gsh/Dockerfile @@ -2,4 +2,4 @@ FROM tier/grouper:latest MAINTAINER tier-packaging@internet2.edu -CMD ["bin/gsh"] +CMD ["gsh"] diff --git a/test-compose/scim/Dockerfile b/test-compose/scim/Dockerfile new file mode 100644 index 0000000..52bfb24 --- /dev/null +++ b/test-compose/scim/Dockerfile @@ -0,0 +1,5 @@ +FROM tier/grouper:latest + +LABEL author="tier-packaging@internet2.edu " + +CMD ["scim"] diff --git a/test-compose/ui/Dockerfile b/test-compose/ui/Dockerfile index 1dd0943..68f566d 100644 --- a/test-compose/ui/Dockerfile +++ b/test-compose/ui/Dockerfile @@ -2,9 +2,6 @@ FROM tier/grouper:latest LABEL author="tier-packaging@internet2.edu " -COPY WEB-INF/ /opt/grouper/grouper.ui/dist/grouper/WEB-INF/ -COPY tomcat/ /opt/tomcat/conf/ - -#COPY httpd/logout.php /var/www/cgi-bin/logout.php +COPY container_files/WEB-INF/ /opt/grouper/grouper.ui/WEB-INF/ CMD ["ui"] diff --git a/test-compose/ui/WEB-INF/web.xml b/test-compose/ui/container_files/WEB-INF/web.xml similarity index 100% rename from test-compose/ui/WEB-INF/web.xml rename to test-compose/ui/container_files/WEB-INF/web.xml diff --git a/test-compose/ui/shibboleth/shibd.logger b/test-compose/ui/container_files/shibboleth/shibd.logger similarity index 100% rename from test-compose/ui/shibboleth/shibd.logger rename to test-compose/ui/container_files/shibboleth/shibd.logger diff --git a/test-compose/ws/Dockerfile b/test-compose/ws/Dockerfile index ea9b4c7..b163f51 100644 --- a/test-compose/ws/Dockerfile +++ b/test-compose/ws/Dockerfile @@ -2,7 +2,7 @@ FROM tier/grouper:latest LABEL author="tier-packaging@internet2.edu " -COPY WEB-INF/ /opt/grouper/grouper.ws/dist/grouper-ws/WEB-INF/ -COPY tomcat/ /opt/tomcat/conf/ +COPY container_files/WEB-INF/ /opt/grouper/grouper.ws/WEB-INF/ +COPY container_files/tomcat/ /opt/tomcat/conf/ -CMD ["/usr/bin/local/ws"] \ No newline at end of file +CMD ["ws"] diff --git a/test-compose/ws/WEB-INF/web.xml b/test-compose/ws/container_files/WEB-INF/web.xml similarity index 100% rename from test-compose/ws/WEB-INF/web.xml rename to test-compose/ws/container_files/WEB-INF/web.xml diff --git a/test-compose/ui/tomcat/server.xml b/test-compose/ws/container_files/tomcat/server.xml similarity index 90% rename from test-compose/ui/tomcat/server.xml rename to test-compose/ws/container_files/tomcat/server.xml index b3b82e5..1b9f22d 100644 --- a/test-compose/ui/tomcat/server.xml +++ b/test-compose/ws/container_files/tomcat/server.xml @@ -1,171 +1,180 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +