Nexus3 Infrastructure Seed Installation

As we are looking to automate our Nexus3 installation, we will be modifying the Dockerfile slightly to adjust initial startup behavior.

labseednexus
version: '3.8'

services:
    nexus:
      image: seed/sonatype/nexus3:1.0.0
      build: .
FROM seed/java/openjdk-8:1.0.0

ARG CONTAINER_UID=2001
ARG CONTAINER_GID=2001
ARG CONTAINER_USER=nexus
ARG CONTAINER_GROUP=nexus

RUN groupadd -g ${CONTAINER_GID} ${CONTAINER_GROUP} \
    && useradd -d /home/${CONTAINER_USER} -u ${CONTAINER_UID} -g ${CONTAINER_GROUP} -m -s /bin/bash ${CONTAINER_USER}

RUN mkdir -p /opt/sonatype/nexus /opt/sonatype/sonatype-work/nexus3 \
    && ln -s /opt/sonatype/sonatype-work/nexus3 /nexus-data \
    && chown -R nexus:nexus /opt/sonatype /nexus-data

ARG NEXUS_VERSION=3.41.0-01
ARG NEXUS_CHECKSUM=ce265e627a665f9e833bf9c6e15a58c739882eb753863062f9e42e83e6f0d844

RUN curl -fsSL https://download.sonatype.com/nexus/3/nexus-${NEXUS_VERSION}-unix.tar.gz -o /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz \
    && sha256sum /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz \
    && echo "${NEXUS_CHECKSUM}  /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz" | sha256sum -c - \
    && tar -xvzf /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz --strip-components=1 -C /opt/sonatype/nexus \
    && rm -rf /tmp/*

COPY nexus-default.properties /opt/sonatype/nexus/etc

USER nexus

CMD ["/opt/sonatype/nexus/bin/nexus","run"]
## DO NOT EDIT - CUSTOMIZATIONS BELONG IN $data-dir/etc/nexus.properties
##
# Jetty section
application-port=8081
application-host=0.0.0.0
nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-http.xml,${jetty.etc}/jetty-requestlog.xml
nexus-context-path=/

# Nexus section
nexus-edition=nexus-pro-edition
nexus-features=\
 nexus-pro-feature

nexus.hazelcast.discovery.isEnabled=true

nexus.scripts.allowCreation=true
nexus.security.randompassword=false

The main change to our default Nexus3 Dockerfile is the addition of the nexus-default.properties that will slightly change how Nexus3 configures itself during the initial start. There are two specific configs in particular that we are most interest in to facilitate our headless install.

nexus.scripts.allowCreation=true
nexus.security.randompassword=false

Script creation allows us to execute new scripts for install while the randompassword ensures that a default known password will be used on first setup to execute automation scripts.

Simply build the final and adjusted nexus3 container image.

docker-compose -f seed/nexus/docker-compose.yml build

Automated Configuration

The first initialization step is using an initialization side car container that will connect to a running nexus3 container to apply configuration. With nexus3, configuration is done via a set of groovy scripts that provide a very extensible configuration mechanism.

labseedinit
/**
 * An example script that handles adding or updating a groovy script via the REST API.
 */
@Grab('org.sonatype.nexus:nexus-rest-client:3.9.0-01')
@Grab('org.sonatype.nexus:nexus-rest-jackson2:3.9.0-01')
@Grab('org.sonatype.nexus:nexus-script:3.9.0-01')
@Grab('org.jboss.spec.javax.servlet:jboss-servlet-api_3.1_spec:1.0.0.Final')
@Grab('com.fasterxml.jackson.core:jackson-core:2.8.6')
@Grab('com.fasterxml.jackson.core:jackson-databind:2.8.6')
@Grab('com.fasterxml.jackson.core:jackson-annotations:2.8.6')
@Grab('com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.8.6')
@Grab('org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.0_spec:1.0.1.Beta1')
@Grab('org.jboss.spec.javax.annotation:jboss-annotations-api_1.2_spec:1.0.0.Final')
@Grab('javax.activation:activation:1.1.1')
@Grab('net.jcip:jcip-annotations:1.0')
@Grab('org.jboss.logging:jboss-logging-annotations:2.0.1.Final')
@Grab('org.jboss.logging:jboss-logging-processor:2.0.1.Final')
@Grab('com.sun.xml.bind:jaxb-impl:2.2.7')
@Grab('com.sun.mail:javax.mail:1.5.6')
@Grab('org.apache.james:apache-mime4j:0.6')
@GrabExclude('org.codehaus.groovy:groovy-all')
import javax.ws.rs.NotFoundException

import org.sonatype.nexus.script.ScriptClient
import org.sonatype.nexus.script.ScriptXO

import org.jboss.resteasy.client.jaxrs.BasicAuthentication
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder

CliBuilder cli = new CliBuilder(
    usage: 'groovy addUpdateScript.groovy -u admin -p admin123 -f scriptFile.groovy [-n explicitName] [-h nx3Url]')
cli.with {
  u longOpt: 'username', args: 1, required: true, 'A User with permission to use the NX3 Script resource'
  p longOpt: 'password', args: 1, required: true, 'Password for given User'
  f longOpt: 'file', args: 1, required: true, 'Script file to send to NX3'
  h longOpt: 'host', args: 1, 'NX3 host url (including port if necessary). Defaults to http://localhost:8081'
  n longOpt: 'name', args: 1, 'Name to store Script file under. Defaults to the name of the Script file.'
}
def options = cli.parse(args)
if (!options) {
  return
}

def file = new File(options.f)
assert file.exists()

def host = options.h ?: 'http://localhost:8081'
def resource = 'service/rest'

ScriptClient scripts = new ResteasyClientBuilder()
    .build()
    .register(new BasicAuthentication(options.u, options.p))
    .target("$host/$resource")
    .proxy(ScriptClient)

String name = options.n ?: file.name

// Look to see if a script with this name already exists so we can update if necessary
boolean newScript = true
try {
  scripts.read(name)
  newScript = false
  println "Existing Script named '$name' will be updated"
}
catch (NotFoundException e) {
  println "Script named '$name' will be created"
}

def script = new ScriptXO(name, file.text, 'groovy')
if (newScript) {
  scripts.add(script)
}
else {
  scripts.edit(name, script)
}

println "Stored scripts are now: ${scripts.browse().collect { it.name }}"
import org.sonatype.nexus.blobstore.api.BlobStoreManager
import org.sonatype.nexus.repository.config.WritePolicy

def blobStoreProxy = blobStore.createFileBlobStore('apt-proxy', 'apt-proxy')

  /**
   * Create an Apt proxy repository.
   * @param name The name of the new Repository
   * @param remoteUrl The url of the external proxy for this Repository
   * @param blobStoreName The BlobStore the Repository should use
   * @param distribution The name of the required distribution
   * @param strictContentTypeValidation Whether or not the Repository should enforce strict content types
   * @return the newly created Repository
   */
   /*
  Repository createAptProxy(final String name,
                            final String remoteUrl,
                            final String blobStoreName,
                            final String distribution,
                            final boolean strictContentTypeValidation);
                            */

repository.createAptProxy('apt-proxy-ubuntu-focal',
                            'http://archive.ubuntu.com/ubuntu/',
                            'apt-proxy',
                            'focal',
                            true)

log.info('Created Apt Ubuntu Focal')

repository.createAptProxy('apt-proxy-ubuntu-focal-microsoft',
                            'https://packages.microsoft.com/ubuntu/20.04/prod/',
                            'apt-proxy',
                            'focal',
                            true)

log.info('Created Apt Ubuntu Focal Microsoft')

repository.createAptProxy('apt-proxy-ubuntu-focal-microsoft-mssql',
                            'https://packages.microsoft.com/ubuntu/20.04/mssql-server-2019',
                            'apt-proxy',
                            'focal',
                            true)

log.info('Created Apt Ubuntu Focal Microsoft SQL')

repository.createAptProxy('apt-proxy-ubuntu-focal-docker',
                            'https://download.docker.com/linux/ubuntu/',
                            'apt-proxy',
                            'focal',
                            true)

log.info('Created Apt Ubuntu Focal Docker')

repository.createAptProxy('apt-proxy-ubuntu-focal-postgres',
                            'https://apt.postgresql.org/pub/repos/apt/',
                            'apt-proxy',
                            'focal-pgdg',
                            true)

log.info('Created Apt Ubuntu Focal Postgres')

log.info('Script aptRepositories completed successfully')
import org.sonatype.nexus.repository.config.WritePolicy

// create a new blob store dedicated to usage with raw repositories
def blobStore = blobStore.createFileBlobStore('raw-build-archive', 'raw-build-archive')

// for any files, only allow once
repository.createRawHosted('build-archive', blobStore.name, false, WritePolicy.ALLOW_ONCE)

log.info('Script buildArchiveRepositories completed successfully')
repository.getRepositoryManager().delete('maven-snapshots');
repository.getRepositoryManager().delete('maven-releases');
repository.getRepositoryManager().delete('maven-central');
repository.getRepositoryManager().delete('maven-public');
repository.getRepositoryManager().delete('nuget-group');
repository.getRepositoryManager().delete('nuget-hosted');
repository.getRepositoryManager().delete('nuget.org-proxy');

log.info('Script cleanupRepositories completed successfully')
@Grab('org.sonatype.nexus:nexus-rest-client:3.9.0-01')
@Grab('org.sonatype.nexus:nexus-rest-jackson2:3.9.0-01')
@Grab('org.sonatype.nexus:nexus-script:3.9.0-01')
@Grab('org.jboss.spec.javax.servlet:jboss-servlet-api_3.1_spec:1.0.0.Final')
@Grab('com.fasterxml.jackson.core:jackson-core:2.8.6')
@Grab('com.fasterxml.jackson.core:jackson-databind:2.8.6')
@Grab('com.fasterxml.jackson.core:jackson-annotations:2.8.6')
@Grab('com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.8.6')
@Grab('org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.0_spec:1.0.1.Beta1')
@Grab('org.jboss.spec.javax.annotation:jboss-annotations-api_1.2_spec:1.0.0.Final')
@Grab('javax.activation:activation:1.1.1')
@Grab('net.jcip:jcip-annotations:1.0')
@Grab('org.jboss.logging:jboss-logging-annotations:2.0.1.Final')
@Grab('org.jboss.logging:jboss-logging-processor:2.0.1.Final')
@Grab('com.sun.xml.bind:jaxb-impl:2.2.7')
@Grab('com.sun.mail:javax.mail:1.5.6')
@Grab('org.apache.james:apache-mime4j:0.6')
@GrabExclude('org.codehaus.groovy:groovy-all')
import javax.ws.rs.NotFoundException

import org.sonatype.nexus.script.ScriptClient
import org.sonatype.nexus.script.ScriptXO

import org.jboss.resteasy.client.jaxrs.BasicAuthentication
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder

import groovy.json.JsonOutput
#!/bin/bash

groovy -Dgroovy.grape.report.downloads=true dependencies.groovy

printf "\nDependencies Loaded\n\n"
version: '3.8'

services:
    nexus-init:
      image: seed/sonatype/nexus3-init:1.0.0
      build: .
FROM seed/ubuntu/focal:1.0.0

RUN apt-get update \
    && apt-get install -y dos2unix netcat groovy curl \
    && apt-get clean

# Preload dependenices for faster init execution
# this will take a while, trust me, you think its hanging, but then no
COPY dependencies.groovy dependencies.groovy
COPY dependencies.sh dependencies.sh
RUN chmod +x dependencies.sh \
    && dos2unix dependencies.sh \
    && ./dependencies.sh

# copy our groovy
COPY addUpdateScript.groovy /init/addUpdateScript.groovy
COPY security.groovy /init/security.groovy
COPY rawRepositories.groovy /init/rawRepositories.groovy
COPY cleanupRepositories.groovy /init/cleanupRepositories.groovy
COPY dockerRepositories.groovy /init/dockerRepositories.groovy
COPY mavenRepositories.groovy /init/mavenRepositories.groovy
COPY aptRepositories.groovy /init/aptRepositories.groovy
COPY buildArchiveRepositories.groovy /init/buildArchiveRepositories.groovy

# wait for so we can do ze wait
ARG WAITFOR_VERSION=2.2.1
ARG WAITFOR_CHECKSUM=e38cb3073a844983aee8780a34959a293dad27006f95ec4e249ef81ded057ca1

RUN curl -fsSL https://github.com/eficode/wait-for/releases/download/v${WAITFOR_VERSION}/wait-for -o /wait-for \
    && sha256sum /wait-for \
    && echo "${WAITFOR_CHECKSUM}  /wait-for" | sha256sum -c - \
    && chmod +x /wait-for

#-------------

# actual init now
COPY init.sh /init/init.sh
RUN chmod +x /init/init.sh \
    && dos2unix /init/init.sh

COPY entry.sh /entry.sh
RUN chmod +x /entry.sh \
    && dos2unix /entry.sh
    
CMD ["./entry.sh"]
import org.sonatype.nexus.blobstore.api.BlobStoreManager
import org.sonatype.nexus.repository.config.WritePolicy

def blobStoreDockerHosted = blobStore.createFileBlobStore('docker-hosted', 'docker-hosted')

repository.createDockerHosted('docker-internal', 8083, null, blobStoreDockerHosted.name, true, true, WritePolicy.ALLOW_ONCE) 

log.info('Created Hosted')

def blobStoreProxy = blobStore.createFileBlobStore('docker-proxy', 'docker-proxy')
repository.createDockerProxy('docker-hub',                   // name
                             'https://registry-1.docker.io', // remoteUrl
                             'HUB',                          // indexType
                             null,                           // indexUrl
                             null,                           // httpPort
                             null,                           // httpsPort
                             blobStoreProxy.name, // blobStoreName 
                             true, // strictContentTypeValidation
                             true  // v1Enabled
                             )

log.info('Created Proxy Docker Hub')

// create group and allow access via https
def groupMembers = ['docker-hub', 'docker-internal']
repository.createDockerGroup('docker-all', 8082, null, groupMembers, true)

log.info('Created Combined')

log.info('Script dockerRepositories completed successfully')
#!/bin/bash

# A simple example script that publishes a number of scripts to the Nexus Repository Manager
# and executes them.

# fail if anything errors
set -e
# fail if a function call is missing an argument
set -u

username=admin
password=admin123

# add the context if you are not using the root context
host=http://${nexushost}:8081

# add a script to the repository manager and run it
function addAndRunScript {
  name=$1
  file=$2

  groovy -Dgroovy.grape.report.downloads=false /init/addUpdateScript.groovy -u "$username" -p "$password" -n "$name" -f "$file" -h "$host"
  printf "\nPublished $file as $name\n\n"
  curl -v -X POST -u $username:$password --header "Content-Type: text/plain" "$host/service/rest/v1/script/$name/run"
  printf "\nSuccessfully executed $name script\n\n\n"
}

printf "Provisioning Integration API Scripts Starting \n\n" 
printf "Publishing and executing on $host\n"

addAndRunScript security /init/security.groovy
addAndRunScript cleanup /init/cleanupRepositories.groovy
addAndRunScript raw /init/rawRepositories.groovy
addAndRunScript docker /init/dockerRepositories.groovy
addAndRunScript maven /init/mavenRepositories.groovy
addAndRunScript maven /init/aptRepositories.groovy
addAndRunScript maven /init/buildArchiveRepositories.groovy

printf "\nProvisioning Scripts Completed\n\n"
import org.sonatype.nexus.blobstore.api.BlobStoreManager
import org.sonatype.nexus.repository.config.WritePolicy
import org.sonatype.nexus.repository.maven.VersionPolicy
import org.sonatype.nexus.repository.maven.LayoutPolicy

// maven hosted
def blobStoreMavenHosted = blobStore.createFileBlobStore('maven-hosted', 'maven-hosted')
repository.createMavenHosted('maven-releases', blobStoreMavenHosted.name, true, VersionPolicy.RELEASE, WritePolicy.ALLOW_ONCE, LayoutPolicy.STRICT)

  /**
   * Create a Maven proxy repository.
   * @param name The name of the new Repository
   * @param remoteUrl The url of the external proxy for this Repository
   * @param blobStoreName The BlobStore the Repository should use
   * @param strictContentTypeValidation Whether or not the Repository should enforce strict content types
   * @param versionPolicy The {@link VersionPolicy} for the Repository
   * @param layoutPolicy The {@link LayoutPolicy} for the Repository
   * @return the newly created Repository
   */
   /*
  Repository createMavenProxy(final String name,
                              final String remoteUrl,
                              final String blobStoreName,
                              final boolean strictContentTypeValidation,
                              final VersionPolicy versionPolicy,
                              final LayoutPolicy layoutPolicy);
                              */

// maven proxy
def blobStoreMavenProxy = blobStore.createFileBlobStore('maven-proxy', 'maven-proxy')
repository.createMavenProxy('maven-central',
                        'https://repo1.maven.org/maven2/',
                        blobStoreMavenProxy.name,
                        false,
                        VersionPolicy.RELEASE,
                        LayoutPolicy.PERMISSIVE)

repository.createMavenProxy('maven-clojars',
                        'https://repo.clojars.org/',
                        blobStoreMavenProxy.name,
                        false,
                        VersionPolicy.RELEASE,
                        LayoutPolicy.PERMISSIVE)

// now maven group
def groupMembers = ['maven-releases', 'maven-central', 'maven-clojars']
repository.createMavenGroup('maven-public', groupMembers, 'default');
import org.sonatype.nexus.repository.config.WritePolicy

// create a new blob store dedicated to usage with raw repositories
def blobStore = blobStore.createFileBlobStore('raw', 'raw')

// for any files, only allow once
repository.createRawHosted('dml', blobStore.name, false, WritePolicy.ALLOW_ONCE)

// for any apt keys
repository.createRawHosted('apt-keys', blobStore.name, false, WritePolicy.ALLOW_ONCE)

log.info('Script rawRepositories completed successfully')
//
// disable anonymous access 
//
security.setAnonymousAccess(false)
log.info('Anonymous access disabled')

The Nexus3 initialization container is responsible for all configuration aspects now that scripting has been turned on.

  1. Disable Anonymous Access, aka you must login to see anything (security.groovy)
  2. Remove All preconfigured Repository information (cleanupRepositories.groovy)
  3. Create the raw repository used for our DML (rawRepositories.groovy)
  4. Create all Docker Proxies and Private Registries (dockerRepositories.groovy)
  5. Create Maven Proxy and Private Repositories (mavenRepositories.groovy)

Steps #4 and #5 include typical examples of added third party Docker Registries or Maven Repos. While we are leveraging the RepositoryAPI in our setup scripts, not all features we need are available; one of them being able to update existing Maven/Docker Groups. To overcome this we simply remove anything that is preconfigured via our cleanupRepositories.groovy and re-add it via API.

For more information on the API and additional scripts such as users etc refer to the following:

As we are leveraging the same ubuntu and JDK image we previously build during our default install, all we need now are builds for the updated nexus3 and nexus3 init.

docker-compose -f seed/nexus-init/docker-compose.yml build

Blue Box of Information: Open Source Changes outside of your control

As we are re-installing our entire infrastructure quite frequently, updating to newer versions to ensure we use the latest LTS, we tend to run into slight hiccups every once in a while. It is important to note that we prefer smaller hiccups more frequently, than waiting a long time frame between re-builds and potentially having a significant effort ahead of us that maybe difficult to estimate and complete. To showcase minor changes that required rework on Nexus3 automation.

As of Oct 2020, three changes in a time frame of 15 months, with each change breaking our existing code on Version upgrades.

Nexus Repository Manager 3.28.0 - 2020 Oct 1st

Refactor of References to automatically create repositories from groovy scripts. Nexus had refactored some class references.

org.sonatype.nexus.repository.storage.WritePolicy

Turned into

org.sonatype.nexus.repository.config.WritePolicy

This little change will likely be the reason for many failures of automated scripts you maybe running when using

org.sonatype.nexus.script.plugin.RepositoryApi

The change was easy to adjust and only had minor impact, but remember, we are working with third party software, that can change at any point in time. It is not uncommon for Open Source Initiatives to go through major refactors in a short time frame and potentially even breaking backwards compatibility. Just as you do with your own code, iterative often, iterate small.

Nexus Repository Manager 3.21.2 - 2020 Mar 23

Disabled Groovy scripts by default. Nice for added security for "default" installs, but needed changes for automation.

Repository Manager 3.17.0 - 2019 Jun 23

Disabled default admin password. Great improvement for security on default installs, but prevented automated and scripted installs as we could no longer rely on the same "initial password" unless we had access to the password file that was created on first startup.

Blue Box of Information: Runtime vs Buildtime

You may see the file dependencies.groovy that appears empty besides imports. This is executed as part of the docker build and ensures that all groovy dependencies for script execution are downloaded at build time. The other groovy files only get executed when the container is running via the init.sh. Without pre-downloading them first, we will have to wait to download the dependency at runtime. So we are shifting the download of dependencies to build time instead to ensure that if any of them fail, they do so at build time and preventing errors to happen while the container is running.

Blue Box of Information: wait-for

We are making use of the tool called "wait-for" (https://github.com/mrako/wait-for) which allows us to easily create chained docker executions. Wait-for is quite popular and is best used in scenarios where a docker container may require time before the process it isolates becomes available. In this case, we want to wait for the nexus3 application to become available first, so we can then properly execute scripts to configure. wait-for does require netcat to be installed so be sure to install it on your distro.

Populating the DML

Leveraging the same side car principle as during the initial configuration, we will be uploading all required files to re-create the steps executed thus far. Those are:

  • Ubuntu Docker Image artifacts
  • OpenJDK8 Binaries
  • Nexus3 Binaries
  • Wait-for Script
labseednexus-dml
SERIAL=20220801
40795f39d0f4d3187c0132c5c1d0cb73195caaa8097e0486d75f8edc8cd109b0  ubuntu-focal-oci-amd64-root.manifest
908896fee17c7c3c7b35981d3e5030afa066f2a757a00df9521390308e15275c  ubuntu-focal-oci-amd64-root.tar.gz
afde2e714e09e78ca4bbeb2700ac706cd7b174fab0b5525edc5fec0458c0bfab  ubuntu-focal-oci-arm64-root.manifest
2099d1899e7e6b110a2bb269edb7e2322e4f75a266f6e562ae3c1c189e06394d  ubuntu-focal-oci-arm64-root.tar.gz
45bfb3d20ad9797fd3442e4179ef4bc28a7681c77bd3c74b8e2484a8afd67e3b  ubuntu-focal-oci-armhf-root.manifest
1cdd75754be4bbb65155133d91d0521cf206839ce5ad41fa0cef1b0df74ad9ac  ubuntu-focal-oci-armhf-root.tar.gz
979c58ab736eaf720ae1244e396590a271ddac602208b28223de53c06efeca81  ubuntu-focal-oci-ppc64el-root.manifest
d3f7688ce2d1bb682783f6b10d37ca0d77e1115384932f29d9bf8b98b973c4ef  ubuntu-focal-oci-ppc64el-root.tar.gz
e5b5dfc3ffe3168e28b965a64f5c8643d619f612268dabd9cd17029f1feeab2d  ubuntu-focal-oci-riscv64-root.manifest
126291630fbacebacfec221e0d3f12653f4857ad989257dc17fe78300dd5d7b0  ubuntu-focal-oci-riscv64-root.tar.gz
d9a475e2eddc1bec1fc75a257095aeced3156f17c0d0a1c0a72309bfe70575ae  ubuntu-focal-oci-s390x-root.manifest
c834e3f65025b7cb79e0f5749ba496fda8e5233ce5a37641ddc0ff7a832a62a9  ubuntu-focal-oci-s390x-root.tar.gz
n/a
adduser	3.118ubuntu2
apt	2.0.9
base-files	11ubuntu5.5
base-passwd	3.5.47
bash	5.0-6ubuntu1.2
bsdutils	1:2.34-0.1ubuntu9.3
bzip2	1.0.8-2
coreutils	8.30-3ubuntu2
dash	0.5.10.2-6
debconf	1.5.73
debianutils	4.9.1
diffutils	1:3.7-3
dpkg	1.19.7ubuntu3.2
e2fsprogs	1.45.5-2ubuntu1.1
fdisk	2.34-0.1ubuntu9.3
findutils	4.7.0-1ubuntu1
gcc-10-base:amd64	10.3.0-1ubuntu1~20.04
gpgv	2.2.19-3ubuntu2.2
grep	3.4-1
gzip	1.10-0ubuntu4.1
hostname	3.23
init-system-helpers	1.57
libacl1:amd64	2.2.53-6
libapt-pkg6.0:amd64	2.0.9
libattr1:amd64	1:2.4.48-5
libaudit-common	1:2.8.5-2ubuntu6
libaudit1:amd64	1:2.8.5-2ubuntu6
libblkid1:amd64	2.34-0.1ubuntu9.3
libbz2-1.0:amd64	1.0.8-2
libc-bin	2.31-0ubuntu9.9
libc6:amd64	2.31-0ubuntu9.9
libcap-ng0:amd64	0.7.9-2.1build1
libcom-err2:amd64	1.45.5-2ubuntu1.1
libcrypt1:amd64	1:4.4.10-10ubuntu4
libdb5.3:amd64	5.3.28+dfsg1-0.6ubuntu2
libdebconfclient0:amd64	0.251ubuntu1
libext2fs2:amd64	1.45.5-2ubuntu1.1
libfdisk1:amd64	2.34-0.1ubuntu9.3
libffi7:amd64	3.3-4
libgcc-s1:amd64	10.3.0-1ubuntu1~20.04
libgcrypt20:amd64	1.8.5-5ubuntu1.1
libgmp10:amd64	2:6.2.0+dfsg-4
libgnutls30:amd64	3.6.13-2ubuntu1.6
libgpg-error0:amd64	1.37-1
libhogweed5:amd64	3.5.1+really3.5.1-2ubuntu0.2
libidn2-0:amd64	2.2.0-2
liblz4-1:amd64	1.9.2-2ubuntu0.20.04.1
liblzma5:amd64	5.2.4-1ubuntu1.1
libmount1:amd64	2.34-0.1ubuntu9.3
libncurses6:amd64	6.2-0ubuntu2
libncursesw6:amd64	6.2-0ubuntu2
libnettle7:amd64	3.5.1+really3.5.1-2ubuntu0.2
libp11-kit0:amd64	0.23.20-1ubuntu0.1
libpam-modules:amd64	1.3.1-5ubuntu4.3
libpam-modules-bin	1.3.1-5ubuntu4.3
libpam-runtime	1.3.1-5ubuntu4.3
libpam0g:amd64	1.3.1-5ubuntu4.3
libpcre2-8-0:amd64	10.34-7
libpcre3:amd64	2:8.39-12ubuntu0.1
libprocps8:amd64	2:3.3.16-1ubuntu2.3
libseccomp2:amd64	2.5.1-1ubuntu1~20.04.2
libselinux1:amd64	3.0-1build2
libsemanage-common	3.0-1build2
libsemanage1:amd64	3.0-1build2
libsepol1:amd64	3.0-1ubuntu0.1
libsmartcols1:amd64	2.34-0.1ubuntu9.3
libss2:amd64	1.45.5-2ubuntu1.1
libstdc++6:amd64	10.3.0-1ubuntu1~20.04
libsystemd0:amd64	245.4-4ubuntu3.17
libtasn1-6:amd64	4.16.0-2
libtinfo6:amd64	6.2-0ubuntu2
libudev1:amd64	245.4-4ubuntu3.17
libunistring2:amd64	0.9.10-2
libuuid1:amd64	2.34-0.1ubuntu9.3
libzstd1:amd64	1.4.4+dfsg-3ubuntu0.1
login	1:4.8.1-1ubuntu5.20.04.2
logsave	1.45.5-2ubuntu1.1
lsb-base	11.1.0ubuntu2
mawk	1.3.4.20200120-2
mount	2.34-0.1ubuntu9.3
ncurses-base	6.2-0ubuntu2
ncurses-bin	6.2-0ubuntu2
passwd	1:4.8.1-1ubuntu5.20.04.2
perl-base	5.30.0-9ubuntu0.2
procps	2:3.3.16-1ubuntu2.3
sed	4.7-1
sensible-utils	0.0.12+nmu1
sysvinit-utils	2.96-2.1ubuntu1
tar	1.30+dfsg-7ubuntu0.20.04.2
ubuntu-keyring	2020.02.11.4
util-linux	2.34-0.1ubuntu9.3
zlib1g:amd64	1:1.2.11.dfsg-2ubuntu1.3
version: '3.3'

services:
    nexus-dml:
      image: seed/dml:1.0.0
      build: .
FROM seed/ubuntu/focal:1.0.0

RUN apt-get update \
    && apt-get install -y curl netcat dos2unix \
    && apt-get clean

#####################################
## UBUNTU FOCAL 20.04
## https://github.com/tianon/docker-brew-ubuntu-core/tree/dist-amd64/focal
#####################################

COPY ubuntu-focal/build-info.txt /tmp/ubuntu-focal/build-info.txt
COPY ubuntu-focal/SHA256SUMS /tmp/ubuntu-focal/SHA256SUMS
COPY ubuntu-focal/ubuntu-focal-oci-amd64-root.tar.gz /tmp/ubuntu-focal/ubuntu-focal-oci-amd64-root.tar.gz
COPY ubuntu-focal/ubuntu-focal-oci-amd64.manifest /tmp/ubuntu-focal/ubuntu-focal-oci-amd64.manifest

#####################################
## OpenJDK 8
## https://github.com/AdoptOpenJDK/openjdk-docker/tree/master/8/jdk/ubuntu
## Dockerfile.hotspot.releases.full
#####################################

ENV JDK8_ESUM=0949505fcf42a1765558048451bb2a22e84b3635b1a31dd6191780eeccaa4ada
ENV JDK8_VERSION=jdk8u292-b10
ENV JDK8_FILE=OpenJDK8U-jdk_x64_linux_hotspot_8u292b10.tar.gz

RUN curl -fsSL https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/${JDK8_VERSION}/${JDK8_FILE} -o /tmp/${JDK8_FILE} \
    #&& sha256sum /tmp/${JDK8_FILE} \
    && echo "${JDK8_ESUM}  /tmp/${JDK8_FILE}" | sha256sum -c -

#####################################
## NEXUS
## https://help.sonatype.com/repomanager3/release-notes
## https://help.sonatype.com/repomanager3/download/download-archives---repository-manager-3
#####################################

ARG NEXUS_VERSION=3.41.0-01
ARG NEXUS_CHECKSUM=ce265e627a665f9e833bf9c6e15a58c739882eb753863062f9e42e83e6f0d844

RUN curl -fsSL https://download.sonatype.com/nexus/3/nexus-${NEXUS_VERSION}-unix.tar.gz -o /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz \
    && sha256sum /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz \
    && echo "${NEXUS_CHECKSUM}  /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz" | sha256sum -c -

#####################################
## WAIT-FOR
## https://github.com/eficode/wait-for
#####################################

ARG WAITFOR_VERSION=2.2.1
ARG WAITFOR_CHECKSUM=e38cb3073a844983aee8780a34959a293dad27006f95ec4e249ef81ded057ca1

RUN curl -fsSL https://github.com/eficode/wait-for/releases/download/v${WAITFOR_VERSION}/wait-for -o /tmp/wait-for \
    #&& sha256sum /tmp/wait-for \
    && echo "${WAITFOR_CHECKSUM}  /tmp/wait-for" | sha256sum -c -

#-------------

# actual init now
COPY init.sh /init/init.sh
RUN chmod +x /init/init.sh \
    && dos2unix /init/init.sh

# we need the wait for in here actually
RUN cp /tmp/wait-for /wait-for \
    && chmod +x /wait-for


COPY entry.sh /entry.sh
RUN chmod +x /entry.sh \
    && dos2unix /entry.sh

CMD ["./entry.sh"]
#!/bin/bash

uploadIfNotExists() {

    echo $1
    echo $2
    echo $3

    # $1 src
    # $2 destination
    # $3 mame

    if curl --output /dev/null --silent --fail -r 0-0 "${NEXUS_BASE_URL}$2"; then
        echo "$3 exists skipping."
    else
        curl -v --user 'admin:admin123' --upload-file $1 ${NEXUS_BASE_URL}$2
    fi 

}

printf "Starting"

# fail if anything errors
set -e

NEXUS_BASE_URL=http://${nexushost}:8081/repository/dml/docker

#########################################

BUILD_INFO=20211006
uploadIfNotExists /tmp/ubuntu-focal/build-info.txt /ubuntu/focal/${BUILD_INFO}/build-info.txt  "ubuntu-buildinfo"
uploadIfNotExists /tmp/ubuntu-focal/SHA256SUMS /ubuntu/focal/${BUILD_INFO}/SHA256SUMS "ubuntu-shasums"
uploadIfNotExists /tmp/ubuntu-focal/ubuntu-focal-oci-amd64-root.tar.gz /ubuntu/focal/${BUILD_INFO}/ubuntu-focal-oci-amd64-root.tar.gz  "ubuntu-tar"
uploadIfNotExists /tmp/ubuntu-focal/ubuntu-focal-oci-amd64.manifest /ubuntu/focal/${BUILD_INFO}/ubuntu-focal-oci-amd64.manifest  "ubuntu-manifest"

JDK8_VERSION=jdk8u292-b10
JDK8_FILE=OpenJDK8U-jdk_x64_linux_hotspot_8u292b10.tar.gz
uploadIfNotExists /tmp/${JDK8_FILE} /jdk/${JDK8_VERSION}/${JDK8_FILE} "jdk8"

NEXUS_VERSION=3.41.0-01
uploadIfNotExists /tmp/nexus-${NEXUS_VERSION}-unix.tar.gz /nexus/nexus-${NEXUS_VERSION}-unix.tar.gz "nexus3"

uploadIfNotExists /tmp/wait-for /tools/wait-for "wait-for"

#########################################

Note: the only files being added directly from local disk are the ubuntu files we previously downloaded for our initial base image (see ubuntu subfolder). All other files such as JDK, Nexus3 are downloaded during the build process.

For application containers it is generally a good practice to "download && unpackage && remove" as chained commands to keep the image layers slim. However, as we are simply isolating a one-time process for the DML, we opt to not only download the binaries, but also keep them on the container itself, ensuring that during build time all files could be downloaded and verified for integrity. End result, at runtime you are guaranteed to have access to your files and simply upload. Finally, you can now access those files either inside the Docker Image, on the DML once uploaded, or on the original download location.

Depending on the number of third party applications and resources you are going to manage, you will end up in a situation where an "original download" location has moved or is no longer accessible at one point, having easy access to the file as needed without having to worry about finding the new download location is always going to be helpful.

Again, for now just build it.

docker-compose -f seed/nexus-dml/docker-compose.yml build

Networking & Hostnames

While we only rely on docker-compose, the exception is the creation of the initial network that all our containers will be running in. This is done via a plain-old docker command.

docker network create --driver=bridge --subnet=172.22.90.0/16 --gateway=172.22.0.1 ops-network

As we work with the network, we also will be creating hostnames for each of our containers that others will used for network communication. Hostnames follow the convention of [environment][environment-number][internal/external]-[host-type]-[descriptor]

Consider the following examples:

  • d1i-win-sql01 - Development Environment #1 Internal (aka not publically reachable) Windows SQL Server 01
  • s2i-doc-app01 - Staging Environment #2 Internal Docker Apps 01

Running the Automated Seed Nexus3 install

We simply start our nexus3 image and attach our two setup containers, the initial config and then the DML container to populate third party files.

labcomposed
version: '3.3'

services:
  nexus:
    image: seed/sonatype/nexus3:1.0.0
    hostname: d1i-doc-nexus01
    volumes:
      - nexus-data:/nexus-data
    ports:
      - 8081:8081
    networks:
      - ops-network
volumes:
  nexus-data:
    name: nexus-data
networks:
  ops-network:
    name: ops-network

As we want to run this as a background process we simply create the resources and the start it.

docker-compose -f composed/docker-compose-seed.yml up --no-start
docker-compose -f composed/docker-compose-seed.yml start

You can optionally check the logs via the below command to confirm Nexus3 has started.

docker-compose -f composed/docker-compose-seed.yml logs -f --tail="50"

The following message will appear: Started Sonatype Nexus OSS [version]. Once started, you can access the instance at http://localhost:8081 with username admin and password admin123.

Next step is starting the configuration container using the following compose compose file.

labcomposed
version: '3.8'

services:
  nexus-init:
    image: seed/sonatype/nexus3-init:1.0.0
    hostname: d1i-doc-nxsinit
    environment:
      nexushost: d1i-doc-nexus01
    networks:
      - ops-network
networks:
  ops-network:
    name: ops-network

Run it via:

docker-compose -f composed/docker-compose-seed-init.yml up

The nexus-init container will start executing scripts once the HTTP port is available at http://localhost:8081. Once the initialization is completed the init process will terminate.

Same with the DML via the following compose file.

labcomposed
version: '3.3'

services:
  nexus-dml:
    image: seed/dml:1.0.0
    hostname: d1i-doc-nxsdml
    environment:
      nexushost: d1i-doc-nexus01
    networks:
      - ops-network
networks:
  ops-network:
    name: ops-network

Run it to start the upload.

docker-compose -f composed/docker-compose-seed-dml.yml up

Same as before, the upload will start once the HTTP port is available at http://localhost:8081. Once the upload is complete, the dml process will terminate.

Summary

The initial part of our seed installation is completed and we now have a Nexus3 Volume fully populated. As all prior steps are meant to be run ONE time, we now want to run the fully installed Nexus3 instance connected to our final volume via the following compose file.

Fully installed and configured repositories
Pre-populated DML