Tuesday, June 16, 2020

PATs - CI/CD Series

Since we now have a fully runnable assembly (jar), we can add Product Acceptance Tests (PATs) to our automated build. PATs are a type of test which tests the product as a black-box - meaning that we work directly with the contracts defined on the exposed API without internal knowledge of the system. This allows us to test the system with more real-world style tests whereas unit tests usually go for a lot more edge-case tests for all possible input scenarios.

In our use-case, we will run our PATs against the defined REST endpoints and ensure the proper response codes and content are returned for each call. We will also simulate scenarios for our guestbook application.

Design

To help build these PATs, we are going to utilize Behavior Driven Development (BDD) Testing - specifically the Cucumber library:
Cucumber and its language Gherkin are a framework which have multiple implementations. For our use-case, we will be using the python implementation called "behave":
The reason for choosing python is just to use something different than the implementation language of our REST service. Also, it helps show that these PATs are completely separate from the actual service.

Test Setup

To run our tests, we will start our REST service as an in-memory process. This is facilitated via Cucumber with before-all and after-all setup stages:

from behave import *
import requests
import subprocess
import time

def before_all(context):
print('Starting server')
process = subprocess.Popen(['java', '-jar', 'cicd-series-assembly.jar'])
time.sleep(2)
print('Saving process to context')
context.proc = process

def after_all(context):
print('Terminating server')
context.proc.terminate()
print('Server terminated')

Running our service as an in-memory process ensures that we are running our tests against the fully built artifact from our "assemble" CI stage. Thus, these tests are running against the jar that we would push to a DEV or PROD environment instead of an one-off build.

Test Implementation

To build our tests, we write the actual test in the Gherkin language. This is a more natural language than most programming languages and can be understood without having much knowledge of its structure. Also, it uses the "Given/When/Then" style which is familiar to BDD Testing:
  • Given = setting up the service to be in a given state
  • When = the action to perform against the service
  • Then = the assertions to perform after the action
For the actual tests, we are building more real-world style use-cases - some of which are very similar to the unit tests we built previously. For example, we can build a test to ensure we cannot add a duplicate guest to our guestbook:

Scenario: conflict if a guest is added twice
Given a guestbook with one guest
When we add a guest
Then the response should be 409
And a single guest should be found with /guests

And with Cucumber, each "Given/When/Then" line maps to actual code. For the above test, our python code looks like:

from behave import *
import requests

@given('a guestbook with one guest')
def step_impl(context):
url = 'http://localhost:8080/guests'
guest = {'name': 'Dan', 'age': 31}
post_response = requests.post(url, json=guest)
assert post_response.status_code == 201
list_response = requests.get(url)
assert list_response.status_code == 200
assert 'guests' in list_response.json()
guests = list_response.json()['guests']
assert len(guests) == 1
assert guests[0] == guest

@when('we add a guest')
def step_impl(context):
url = 'http://localhost:8080/guests'
json = {'name': 'Dan', 'age': 31}
post_response = requests.post(url, json=json)
context.response = post_response

@then('the response should be 409')
def step_impl(context):
assert context.response.status_code == 409

@then('a single guest should be found with /guests')
def step_impl(context):
url = 'http://localhost:8080/guests'
guest = {'name': 'Dan', 'age': 31}
list_response = requests.get(url)
assert list_response.status_code == 200
assert 'guests' in list_response.json()
guests = list_response.json()['guests']
assert len(guests) == 1
assert guests[0] == guest

CI/CD

Now that we have our tests setup, we can plug them into our automated builds. Again, this will ensure that our system adheres to its contracts on every build and should something fail we will get automated build failure notifications.

The first step in plugging these tests into our build is to pass the built artifact from our "assemble" stage to our "pats" stage. We want to pass the built artifact between stages since we already have a dedicated stage to ensuring the build works as expected, hence, there is no reason to do the build twice. This artifact passing can be done using GitHub Actions:
Next, we want to define our PAT stage within our build. Since we are running both a Java application and Python tests, we need to choose a docker image which has all of our prerequisites installed by default (or build a custom image). Luckily, there is a docker image available which has Java 8 and Python 3 installed:
After that, we just need to install the required python dependencies and run our tests with "behave":

pats:
runs-on: ubuntu-latest
container: openkbs/jre-mvn-py3:v1.0.6
needs: assemble

steps:
- name: Checkout Repo
uses: actions/checkout@v2

# Download artifact
- name: Download Artifact
uses: actions/download-artifact@v2
with:
name: cicd-series-assembly.jar

# Verify artifact
- name: List Files
run: ls -al

# This is needed because the artifact is downloaded with the original file name (includes version)
- name: Rename Artifact
run: mv cicd-series-assembly-*.jar cicd-series-assembly.jar

# This is needed because download artifacts are not runnable
- name: Change Permissions
run: chmod a+rx cicd-series-assembly.jar

# Verify artifact
- name: List Files
run: ls -al

# Install python dependencies
- name: Install Dependencies
run: pip install -r requirements.txt

# Run behave tests
- name: Run PATs
run: behave

Conclusion

We now have added automated PATs to run on every Pull Request to the master branch of our repo. They were built using python and the Cucumber library to perform BDD Tests. Also, should anything fail, we will get automated build failure notifications.

All of the code above and more can be found on this pull request.

Sunday, June 14, 2020

Assembly - CI/CD Series

Now that we have a basic REST service setup and continually running unit tests, we can now work on building a runnable assembly for the service. To do this, we'll leverage:
Project Build

To make use of the sbt-assembly plugin, we first need to enable the plugin by adding the following to our "project/plugins.sbt" file:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")

After that is done, we need to define how to build the assembly:

mainClass in assembly := Some("com.github.dwiechert.Main"),

// Ignore tests when running the "assembly" task
test in assembly := {}

The definition above indicates:
  • The main class to be ran
  • Ignore unit tests when running the "assembly" phase
The reason why the tests are ignored is because in the previous post we integrated automated testing in a separate GitHub Actions step. Since we already have unit tests running there is no need to re-run them again when we build the final assembly. It is possible to run both the tests and assembly as one stage, but then debugging issues with the build could be more troublesome. With two distinct jobs (unit test and assemble), we can quickly and easily determine which stage failed. Whereas if they both ran on the same stage we would need to look into the logs to see if the build failed due to unit tests or the building of the jar itself.

Once these settings are in place, we can build the jar with a simple sbt command:

sbt "clean;compile;assembly"

To test that everything works, we can run the jar locally and ensure it starts up correct:

java -jar target/scala-2.13/cicd-series-assembly-0.1.0-SNAPSHOT.jar

CI/CD Setup

Given that we now have a runnable jar, we need to integrate this with our current CI/CD setup. The reason to integrate this build into our CI/CD system is to ensure not only the unit tests pass, but also that the jar can be built properly. 

To do this, we're going to add a new job in our GitHub Actions file which will run the same assembly commands mentioned above:

assemble:
# The type of runner that the job will run on
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
runs-on: ubuntu-latest
# The specific container to use
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainer
# https://hub.docker.com/r/hseeberger/scala-sbt/
container: hseeberger/scala-sbt:8u222_1.3.5_2.13.1

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repo
uses: actions/checkout@v2

# Assembly artifact
- name: Create Assembly
run: sbt "clean;compile;assembly"

As mentioned above, this is a new GitHub Actions job. The assembly will run in parallel to the job which runs the unit tests we previously created.

In addition to just building our assembly, we will store the jar as an artifact of the build. This will allow us to re-use this jar in later jobs that will be build in the future (PATs, deployment).

# Upload jar as build artifact
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: cicd-series-assembly.jar
path: target/scala-2.13/cicd-series-assembly-0.1.0-SNAPSHOT.jar

Conclusion

We now have a way to not only build a runnable jar for our service but this step happens on each pull request to master. This will help ensure the state of our system is always tracked and can be deployed at any time.

All of the changes discussed can be found on this pull request.

Sunday, June 7, 2020

Unit Tests - CI/CD Series

In the previous post we created a simple REST service built on top of akka-http. However, as practiced with professional software development, we want to add unit tests to our service to ensure everything is working as planned. These unit tests provide several things for our service:
  1. Automated testing
  2. Reproducible tests
  3. Software assurance
Unit Tests

To build the unit tests for our service, we will leverage:
  • ScalaTest
    • The base testing framework (similar to JUnit for Java)
  • akka-http
    • akka-http provides test harnesses that integrate directly with ScalaTest
With these testing libraries, we can perform tests directly against the endpoints of our service instead of using objects. Also, both the requests and responses are full payloads so we can assert things such as:
  • Response code
  • Application type
  • Response body
This will help provide a full end-to-end unit test instead of directly calling an object and ignoring the serialization aspect of the test.

The simplest endpoint in our service is the health check - it only returns 200. A unit test for this endpoint is as simple as:

it should "return OK for /health" in {
Get("/health") ~> healthCheck.route ~> check {
status shouldBe StatusCodes.OK
}
}

In this test, the actions performed are:
  1. Send a GET request with the path "/health"
  2. The request goes to the route defined in the object "healthCheck"
  3. Assert that the returned status is OK (200)
We can use this same kind of test setup for a more complex case (such as adding a guest to our guestbook):

it should "add a guest" in {
val guestBook = new GuestBook

Post("/guests").withEntity(guestEntity) ~> guestBook.route ~> check {
status shouldBe StatusCodes.Created
}

Get("/guests") ~> guestBook.route ~> check {
status shouldBe StatusCodes.OK
contentType shouldBe ContentTypes.`application/json`
entityAs[Guests] shouldBe Guests(List(guest))
}
}

This test has a similar setup as the one above, just with a few more steps:
  1. Send a POST request with the guest data (defined outside of snippet)
  2. The request goes to the route defined in the object "guestBook"
  3. Assert that the returned status is Created (201)
  4. Send a GET request with the path "/guests"
  5. The request goes to the route defined in the object "guestBook"
  6. Assert that the returned status is OK (200)
  7. Assert that the returned content type is "application/json"
  8. Assert that the returned entity is our guest we added previously (wrapped in a list)
Continuous Integration

Now that we have unit tests available, we can hook up our continuous integration using:
For this simple REST service, the actions we want to perform are:
  • Run unit tests on all pull requests to the "master" branch
  • Run unit tests on all pushes to the "master" branch
  • Running unit tests consists of
    • Using a docker image which has sbt installed
    • Running "sbt test"
The entirety of the above can be expressed with just a few lines of a YAML definition:

name: SBT CI

# Run SBT tests on pushes and pull requests to master branch
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
runs-on: ubuntu-latest
# The specific container to use
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainer
# https://hub.docker.com/r/hseeberger/scala-sbt/
container: hseeberger/scala-sbt:8u222_1.3.5_2.13.1

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2

# Runs a set of commands using the runners shell
- name: Run Unit Tests
run: sbt test

Conclusion

Given the above unit tests and GitHub Actions definition, we now have a system which will run all unit tests on every build. This will not only ensure our system performs as expected, but also alert us when a build breaks for any reason.

All of the changes mentioned above (and more) can be found on this pull request.

Monday, June 1, 2020

Base Application - CI/CD Series

As discussed in the introduction, we are working on a basic REST service with the goal of implementing full end-to-end CI/CD. To help build this REST service, we are leveraging:
All of the changes have been completed and the full changeset can be found on this pull request.

The service currently has the following endpoints:
  • GET /health
    • Basic health check for the service
    • Returns 200
  • GET /guests
    • Returns all guests in the guestbook
    • Returns 200
    • Returns JSON list of guests
  • POST /guests
    • Adds a guest to the guestbook
    • Request is JSON guest {"name":"Dan", "age":31}
    • Returns 201 on creation
    • Returns 409 if the guest already exists
  • DELETE /guests/<name>
    • Deletes a guest by name
    • Returns 204 if the guest was deleted
    • Returns 404 if the guest to delete was not found
Overall, this is an extremely basic REST service, however it will serve our purposes for the rest of this CI/CD series.

In the following posts, we will start to:
  1. Add automated unit tests
  2. Run the service via a Docker image
  3. Add automated product acceptance tests (PATs)
  4. Add automated deployments to a cloud service

Wednesday, May 27, 2020

Introduction - CI/CD Series

Over the last several years, the terms CI/CD (continuous integration, continuous deployment) and DevOps (development and operations) have grown to be known throughout most of the software industry. The implication of these practices is that no longer must software wait for a "release ritual" to push a new version to production. Nor must software be developed by one team and then "handed over" to a production support team. 

Instead, now a development team manages the software from end-to-end:
  • Requirements
  • Code changes
  • Testing
  • Build and package artifacts
  • Deploy to production
  • Support
Also, software has seen a much quicker release cycles. It is even possible to have pipelines that perform releases and deployments upon every committed code change.

Throughout the next few posts, we will build a fully automated CI/CD pipeline for a simple REST service. The series will be broken down into individual posts:
  1. Introduction (this post)
  2. Base application
  3. CI for testing
  4. Dockerizing the application
  5. Product acceptance testing (PATs)
  6. CD for deployment
The tools we will be working with throughout this project will be:

Friday, December 4, 2015

rosetta-stone

One thing that I've come to realize while being a Software Engineer is that I enjoy learning different programming languages, build tools, frameworks, libraries, etc in programming. Now, almost every language/framework/library has their own "How To" guide, but it's usually very basic - how to setup your environment, how to write your first line of code, how to compile, etc. I thought it would be fun to make a "How To" for programming tasks - print to the user, comparison, sorting, etc. That's when I stumbled up Rosetta Code's Programming Tasks page, which has an enormous list of different tasks done in various languages. Once I found this, I decided to make rosetta-stone.

rosetta-stone is a repository which has examples of various programming tasks done in various languages. The tasks that I have completed so far are:

  • hello-world - Print "Hello World!" to the standard out.
  • command-line-arguments - Print the total number of arguments and each argument to the standard out.
  • integer-comparison - Compare two integers and see if the first one is less than, equal to, or greater than the second.
  • string-comparison - Compare two strings and see if they are equal with and without case and their lexicographical ordering.
For each task, I currently have examples in Java, Scala, and Clojure, however I plan on expanding the number of languages in the future. I also plan on adding new tasks and categories as I progress.

Stay tuned for updates on this repository for new tasks and programming languages.

Saturday, October 24, 2015

Crypto-Tool

When downloading files from websites, there are usually MD5 and SHA-1 checksums pasted on the site next to the download link. The reason for these checksums is so one can verify the integrity of the downloaded file. Once the file has been downloaded, you run a local hash of the file and verify that your checksum matches the one pasted on the site. Should they match, you know that the file hasn't been tampered with or you downloaded an incorrect file (though there is a minute chance of a hash collision).

Now this sounds like a really easy way to guarantee that you have downloaded the correct file from a website, but how many people actually run a local hash and check their checksums? I know I don't do it (even though I know I should), and I am willing to bet a lot of other people don't do this either. I looked online for solutions and there seems to be an abundance of utility tools to do this, even Windows has some built in commands.

However, I download things on multiple computers that run multiple OSes and none of these utilities seem to be able to run on all of my devices - and I like programs which run the same on everything. So, this is why I've decided to start my crypto-tool project - a cross-platform crypto tool. It is built in Java (which runs on most all devices) and will allow me to have the same process of checking checksums on every device I use.

The program currently comes with a command line interface which allows the user to generate checksums for input strings and files. It currently supports the following hash algorithms:
  • MD2
  • MD5
  • SHA-1
  • SHA-256
  • SHA-384
  • SHA-512
And to run the program, the user just needs to specify the hash algorithm to use and the file(s) and/or string(s) to generate the checksum for:

 S:\Workspaces\Github - Java\crypto-tool\latest-build>java -jar crypto-tool-cli-0.0.4-jar-with-dependencies.jar hash -ha MD5 -f input.txt -s "Command line string to hash." -s "And another string."  
 MD5 hash of [Command line string to hash.] is: 4354cb305345dc2582f1aefef124df47  
 MD5 hash of [And another string.] is: a3482fe6387ebb8cc20ea5aa713c3b8a  
 MD5 hash of file [input.txt] is: 632e60b155c9a24d3cf47f072532b440  

I feel like this is a good start to the project, however I have some plans to enhance this utility:

  • Add checksum verification
  • Add encryption/decryption of files/strings
  • Add a graphical user interface in addition to the command line interface