Setting up a Test Project

For the purpose of testing our build processes, we will be setting up a simple Hello World Spring Boot Microservice infrastructure to build the java artifacts and also build docker images.

The test project will consists of the following applications:

  • Spring Cloud Gateway
  • Spring Cloud Service Registrar (eureka)
  • Hello World static app
  • Hello World Java API

The source for the project is managed via three distinct monorepos.

  • helloworld source repo containing the java source code
  • helloworld-ops ops repo containing jenkinsfiles and versioned docker images
  • helloworld-config-uat config repo containing UAT environment based config files for all apps

The above projects will be created against the previously installed Bitbucket instance. It will leverage the Bitbucket Server API to create a new project, repositories and finally do initial git commits.

labtoolsbitbucket-helloworld
version: '3.3'

services:
  bitbucket-helloworld:
    image: infra/tools/bitbucket-helloworld:1.0.0
    build:
      context: .
      network: host
      args:
        ARG_ART_URL: http://d1i-doc-ngbuild:3001
      extra_hosts:
        - "d1i-doc-ngbuild:172.22.90.2"
    networks:
      - ops-network
networks:
  ops-network:
    name: ops-network
FROM infra/ubuntu/focal:1.0.0

ARG ARG_ART_URL

RUN sed -e "s|APT_URL|${ARG_ART_URL}|" /etc/apt/sources.list.base > /etc/apt/sources.list \
    && apt-get update \
    && apt-get install -y curl dos2unix git \
    && apt-get clean \
    && rm /etc/apt/sources.list


COPY src/ /init/src/

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

ENTRYPOINT ["/init/init.sh"]
#!/bin/bash

# fail if anything errors
set -e

#https://docs.atlassian.com/bitbucket-server/rest/5.9.0/bitbucket-rest.html

##########################
## Create Projec / Repos
##########################

# we can re-run to delete it all
curl -X DELETE -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY/repos/helloworld-config-uat
curl -X DELETE -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY/repos/helloworld-ops
curl -X DELETE -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY/repos/helloworld
curl -X DELETE -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY

# Create Project & Repos
curl -X POST -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects -d '{"name": "helloworld", "key": "HEY", "description": "Hello World Test Project","is_private": false}'
curl -X POST -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY/repos -d '{"name": "helloworld", "scmId": "git", "forkable": true}'
curl -X POST -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY/repos -d '{"name": "helloworld-ops", "scmId": "git", "forkable": true}'
curl -X POST -v -u admin:admin123 -H "Content-Type: application/json" http://d1i-doc-bitbucket01:7990/rest/api/1.0/projects/HEY/repos -d '{"name": "helloworld-config-uat", "scmId": "git", "forkable": true}'

##########################
## Global Configs
##########################

git config --global user.email "admin@acme.com"
git config --global user.name "Admin User"

##########################
## Add Source
##########################

mkdir  /init/tmp
cd /init/tmp

git clone http://admin:admin123@d1i-doc-bitbucket01:7990/scm/hey/helloworld.git
cd /init/tmp/helloworld
cp -a /init/src/helloworld/* /init/tmp/helloworld

git add --all
git commit -m "Initial Commit"
git push

cd /init/tmp
git clone http://admin:admin123@d1i-doc-bitbucket01:7990/scm/hey/helloworld-ops.git
cd /init/tmp/helloworld-ops
cp -a /init/src/helloworld-ops/* /init/tmp/helloworld-ops

git add --all
git commit -m "Initial Commit"
git push

cd /init/tmp
git clone http://admin:admin123@d1i-doc-bitbucket01:7990/scm/hey/helloworld-config-uat.git
cd /init/tmp/helloworld-config-uat
cp -a /init/src/helloworld-config-uat/* /init/tmp/helloworld-config-uat

git add --all
git commit -m "Initial Commit"
git push
n/a

Building and Running

Build it and run it.

docker-compose -f tools/bitbucket-helloworld/docker-compose.yml build
docker-compose -f tools/bitbucket-helloworld/docker-compose.yml up

You can now login to your previously installed Bitbucket Server instance to view the new project and the three repositories.

Test Project Details

If you want to run the test project, either inside your Java IDE, or docker images or a deployed UAT version make the following changes to your hostfile.

127.0.0.1	dev.acme.com
127.0.0.1	dev-docker.acme.com
127.0.0.1	uat.acme.com

Alternatively, you can change the gateway application properties file for each environment to whatever hostname you like.

Jenkinsfiles

For our testing purposes, we will be using a variety of Jenkinsfiles to be able to mimic simple build processes. As we are using a monorepo for our 4 projects, we will be using "check-if-exists" build approach to ensure we only build what is needed. This applies to the traditional Java projects but also our docker java app equivalents. Further, we are also including a Sonarqube file for the later add-on step.

  • jenkins/Jenkinsfile - Java Maven build that triggers if project does not have any artifacts inside Nexus3. Once build, artifacts are uploaded to Nexus3
  • jenkins/Jenkinsfile-local - as as above but excluding Nexus3 and always building all projects
  • jenkins/Jenkinsfile-sonar - sonarqube build process following the same approach of running and submitting via sonarqube if the project has not yet been analysed
  • jenkinsfile-ops/Jenkinsfile - docker image build for all applications again only building abd publishing if that specific image version does not exist in the Nexus3 docker registry
labtoolsbitbucket-helloworld
import groovy.json.JsonSlurper

pipeline {
    agent {
        label "built-in"
    }
    
    environment {
        NEXUS_VERSION = "nexus3"
        NEXUS_PROTOCOL = "https"
        NEXUS_URL = "nexus.acme.com"
        SCM_URL = "https://bitbucket.acme.com/scm/hey/helloworld.git"
    }
    stages {
        stage("clone-java") {
            steps {
                script {
                    git credentialsId: 'bitbucket', url: SCM_URL;
                }
            }
        }
        stage("builds-java") {
            steps {
			
                dir('infra-discovery'){
                    script {
                        if (!isInNexus()) {
                            sh "mvn clean install"
                            uploadToNexus();
                        }
                    }
                }
                dir('infra-gateway'){
                    script {
                        if (!isInNexus()) {
                            sh "mvn clean install"
                            uploadToNexus();
                        }
                    }
                }
                dir('helloworld-api'){
                    script {
                        if (!isInNexus()) {
                            sh "mvn clean install"
                            uploadToNexus();
                        }
                    }
                }
                dir('helloworld-web'){
                    script {
                        if (!isInNexus()) {
                            sh "mvn clean install"
                            uploadToNexus();
                        }
                    }
                }
            }
        }
    }
}

boolean isInNexus() {

	pom = readMavenPom file: "pom.xml";
	
	nexusUrl = "${NEXUS_PROTOCOL}://${NEXUS_URL}/service/rest/v1/search/assets?group=${pom.groupId}&name=${pom.artifactId}&version=${pom.version}&maven.extension=jar&maven.classifier";

    response = httpRequest authentication: "nexus", url: nexusUrl, ignoreSslErrors: true;
    
    JsonSlurper jsonSlurper = new JsonSlurper();
    Object object = jsonSlurper.parseText(response.getContent());
    
    echo "${object}"

    result = object.items.size() > 0;
   
    return result;
}

void uploadToNexus() {
    
    pom = readMavenPom file: "pom.xml";

    filesByGlob = findFiles(glob: "target/*.${pom.packaging}");

    echo "${filesByGlob[0].name} ${filesByGlob[0].path} ${filesByGlob[0].directory} ${filesByGlob[0].length} ${filesByGlob[0].lastModified}"

    artifactPath = filesByGlob[0].path;
	
    artifactExists = fileExists artifactPath;
	
    if(artifactExists) {
        echo "*** File: ${artifactPath}, group: ${pom.groupId}, packaging: ${pom.packaging}, version ${pom.version}";
		
        nexusArtifactUploader(
            nexusVersion: NEXUS_VERSION,
            protocol: NEXUS_PROTOCOL,
            nexusUrl: NEXUS_URL,
            groupId: pom.groupId,
            version: pom.version,
            repository: "maven-releases",
            credentialsId: "nexus",
            artifacts: [
                // Artifact generated such as .jar, .ear and .war files.
                [artifactId: pom.artifactId,
                classifier: '',
                file: artifactPath,
                type: pom.packaging],
                // Lets upload the pom.xml file for additional information for Transitive dependencies
                [artifactId: pom.artifactId,
                classifier: '',
                file: "pom.xml",
                type: "pom"]
            ]
        );
    } else {
        error "*** File: ${artifactPath}, could not be found";
    }
}
pipeline {
    agent {
        label "built-in"
    }
    
    stages {
        stage("clone") {
            steps {
                script {
                    git credentialsId: 'bitbucket', url: 'https://bitbucket.acme.com/scm/hey/helloworld.git';
                }
            }
        }
        stage("builds") {
            steps {
			
                dir('infra-discovery'){
                    script {
                        sh "mvn clean install"
                    }
                }
                dir('infra-gateway'){
                    script {
                        sh "mvn clean install"
                    }
                }
                dir('helloworld-api'){
                    script {
                        sh "mvn clean install"
                    }
                }
                dir('helloworld-web'){
                    script {
                        sh "mvn clean install"
                    }
                }
            }
        }
    }
}
import groovy.json.JsonSlurper

pipeline {
    agent {
        label "built-in"
    }
    
    environment {
        SONAR_HOST = "https://sonarqube.acme.com"
    }
    stages {
        stage("clone-java") {
            steps {
                script {
                    git credentialsId: 'bitbucket', url: 'https://bitbucket.acme.com/scm/hey/helloworld.git';
                }
            }
        }
        stage("verifies-java") {
            steps {
			
                dir('infra-discovery'){
                    script {
                        if (!isInSonar()) {
                            sh "mvn clean install -Dmaven.test.skip=true"
                            withCredentials([string(credentialsId: 'sonar-token', variable: 'TOKEN')]) {
                                sh('mvn sonar:sonar ${SONAR_SCANNER_OPTS} -Dsonar.projectKey=com.acme:infra-discovery -Dsonar.host.url=${SONAR_HOST} -Dsonar.login=$TOKEN')
                            }
                        }
                    }
                }
                dir('infra-gateway'){
                    script {
                        if (!isInSonar()) {
                            sh "mvn clean install -Dmaven.test.skip=true"
                            withCredentials([string(credentialsId: 'sonar-token', variable: 'TOKEN')]) {
                                sh('mvn sonar:sonar ${SONAR_SCANNER_OPTS} -Dsonar.projectKey=com.acme:infra-gateway -Dsonar.host.url=${SONAR_HOST} -Dsonar.login=$TOKEN')
                            }
                        }
                    }
                }
                dir('helloworld-api'){
                    script {
                        if (!isInSonar()) {
                            sh "mvn clean install -Dmaven.test.skip=true"
                            withCredentials([string(credentialsId: 'sonar-token', variable: 'TOKEN')]) {
                                sh('mvn sonar:sonar ${SONAR_SCANNER_OPTS} -Dsonar.projectKey=com.acme:helloworld-api -Dsonar.host.url=${SONAR_HOST} -Dsonar.login=$TOKEN')
                            }
                        }
                    }
                }
                dir('helloworld-web'){
                    script {
                        if (!isInSonar()) {
                            sh "mvn clean install -Dmaven.test.skip=true"
                            withCredentials([string(credentialsId: 'sonar-token', variable: 'TOKEN')]) {
                                sh('mvn sonar:sonar ${SONAR_SCANNER_OPTS} -Dsonar.projectKey=com.acme:helloworld-web -Dsonar.host.url=${SONAR_HOST} -Dsonar.login=$TOKEN')
                            }
                        }
                    }
                }
            }
        }
    }
}

boolean isInSonar() {

	pom = readMavenPom file: "pom.xml";

    sonarUrl = "${SONAR_HOST}/api/project_analyses/search?project=${pom.groupId}:${pom.artifactId}"

    echo "${sonarUrl}"

    response = httpRequest authentication: "sonar", url: sonarUrl, ignoreSslErrors: true;
    
    JsonSlurper jsonSlurper = new JsonSlurper();
    Object object = jsonSlurper.parseText(response.getContent());
    
    for (Object item : object.analyses) {
        if (item.projectVersion == "${pom.version}") {
            return true;
        }
    }

    return false;
}
pipeline {
    agent {
        label "built-in"
    }
    
    environment {
        NEXUS_PROTOCOL = "https"
        NEXUS_URL = "docker-private.acme.com"
        SCM_URL = "https://bitbucket.acme.com/scm/hey/helloworld-ops.git"
    }
    stages {
        stage("clone-ops") {
            steps {
                script {
                    git credentialsId: 'bitbucket', url: SCM_URL;
                }
            }
        }
        stage("builds-ops") {
            steps {
                script {
                    // dont really need the protocol but keeping it here
                    sh "docker login ${NEXUS_PROTOCOL}://${NEXUS_URL}"
                }
                dir('docker/infra-gateway'){
                    script {
                        if (!isDockerInNexus("infra-gateway")) {
                            buildAndPublish("infra-gateway");
                        }
                    }
                }
                dir('docker/infra-discovery'){
                    script {
                        if (!isDockerInNexus("infra-discovery")) {
                            buildAndPublish("infra-discovery");
                        }
                    }
                }
                dir('docker/helloworld-api'){
                    script {
                        if (!isDockerInNexus("helloworld-api")) {
                            buildAndPublish("helloworld-api");
                        }
                    }
                }
                dir('docker/helloworld-web'){
                    script {
                        if (!isDockerInNexus("helloworld-web")) {
                            buildAndPublish("helloworld-web");
                        }
                    }
                }
                script {
                    sh "docker logout"
                }
            }
        }
    }
}
def buildAndPublish(String group) {

    // get from FROM first
    pull()

    // then build and publish
    sh "docker-compose build"
    def image = getImageToPush(group);
    sh "docker tag ${image} ${NEXUS_URL}/${image}"
    sh "docker push ${NEXUS_URL}/" + image
    sh "docker image rm ${NEXUS_URL}/" + image
}
def pull() {
    def dockerfile = readYaml file: "Dockerfile"
    def lines = dockerfile.readLines()

    def tag = lines[0].substring(5);

    sh "docker pull ${NEXUS_URL}/${tag}"
    sh "docker tag ${NEXUS_URL}/${tag} ${tag}"
    sh "docker image rm ${NEXUS_URL}/${tag}"
}
boolean isDockerInNexus(String group) {

    def yaml = readYaml file: "docker-compose.yml"
    def image = yaml.services[group]["image"];
    def name = image.substring(0, image.lastIndexOf(":"))

    def version = image.substring(image.lastIndexOf(":") + 1, image.length())

    nexusUrl = "${NEXUS_PROTOCOL}://${NEXUS_URL}/v2/${name}/manifests/${version}";
    response = httpRequest authentication: "nexus", url: nexusUrl, ignoreSslErrors: true, validResponseCodes: '200:404';
    
    return response.status != 404;
}

String getImageToPush(String group) {

    def yaml = readYaml file: "docker-compose.yml"
    def image = yaml.services[group]["image"];
    return image;
}