Pipeline as code with a Spring Boot application
This is the last in a serie of posts about continuous delivery based on my local Docker compose stack (see the first and second post here). In this post I use a simple Spring Boot project to show how to make use of the ‘pipeline as code‘ concept. Please note that this is only an example and much, much more is possible. The application I use is taken from the Spring Boot site. The Jenkinsfile is inspired by the one in this post but I had to modify some things to have it work with my stack. The sources of my project can be found here. I will explain the most important snippets in this post.
The pipeline I use contains the following stages:
build stage
In the build stage I make use of the GitLab plugin to checkout the sources of my project. I also put the current commitId in a textFile in the work directory. Next I use Maven (the one we called ‘M3’ in the Jenkins configuration as I described here) to package the code. I also make sure the commitId is passed as parameter to Maven.
deploy stage
in the deploy step I shutdown a running instance of the application by posting ‘true’ to the /shutdown path. Then I simply run the jar I built in the previous step. After that the job waits until the application responds to a simple request.
smoke test
In this simple test step I compare the returned commitId of my deployed service with the commitId we got when I checked out the latest committed code. If everything went well these two id should match, if not something in the chain went wrong.
That’s all for this example. Lets see what this means for the source code. Since it is a Maven project I start with the pom.xml:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <!-- used for metrics like status, health etc --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <!-- used for unit tests --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
No special dependencies are necessary for this project. The ‘spring-boot-starter-web‘ is used for our REST controller. The ‘sprint-boot-starter-actuator‘ can be used for checking the health and much more.
Finally the ‘spring-boot-starter-test’ is used to be able to (unit) test the controller.
Lets have a look at the Java sources. The Application just starts the Spring Boot application. The Controller class is also very basic:
package hello; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; @RestController public class HelloController { @RequestMapping("/") public String index() { return "Greetings from Spring Boot!"; } }
As you can see I simply return a fixed string when a GET request is coming in at ‘/’. The test class has the following test code:
/** * Created by pascal on 19/01/2017. */ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class HelloControllerTest { @Autowired private MockMvc mvc; @Test public void getHello() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string(equalTo("Greetings from Spring Boot!"))); } }
This is also straightforward I guess, I expect the fixed string as a response to a GET request. Next to the Java code there is the ‘application.properties’ file:
server.port=8888 info.app.name=@project.name@ info.app.description=@project.description@ info.app.version=@project.version@ info.app.commitid=@commitid@ endpoints.shutdown.enabled=true
Besides two functional properties, the port we are running the application on (8888) and the ability to shutdown the application by calling the endpoint (endpoints.shutdown.enabled=true), the rest is meant to be shown when calling the endpoint ‘/info’. The parameters @…@ will be replaced with real values by Maven since we filter the resources:
... <resources> <!-- used for variable substitution in application.properties --> <!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.3-Release-Notes#maven-resources-filtering --> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> ...
Finally we have the Jenkinsfile in the project:
import groovy.json.JsonSlurper; properties([[$class: 'GitLabConnectionProperty', gitLabConnection: 'my-gitlab-connection']]) node{ stage 'Build, Test and Package' env.PATH = "${tool 'M3'}/bin:${env.PATH}" checkout scm // workaround, taken from https://github.com/jenkinsci/pipeline-examples/blob/master/pipeline-examples/gitcommit/gitcommit.groovy def commitid = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() def workspacePath = pwd() sh "echo ${commitid} > ${workspacePath}/expectedCommitid.txt" withMaven( maven: 'M3', mavenSettingsConfig: 'a1adf035-653b-410d-b5a6-16b6da77b322', mavenLocalRepo: '.repository') { // Run the maven build sh "mvn clean package -Dcommitid=${commitid}" } } node{ stage 'Stop, Deploy and Start' // shutdown sh 'curl -X POST http://localhost:8888/shutdown || true' // copy file to target location sh 'cp target/*.jar /tmp/' // start the application sh 'nohup java -jar /tmp/*.jar &' // wait for application to respond sh 'while ! httping -qc1 http://localhost:8888 ; do sleep 1 ; done' } node{ stage 'Smoketest' def workspacePath = pwd() sh "curl --retry-delay 10 --retry 5 http://localhost:8888/info -o ${workspacePath}/info.json" if (deploymentOk()){ return 0 } else { return 1 } } def deploymentOk(){ def workspacePath = pwd() expectedCommitid = new File("${workspacePath}/expectedCommitid.txt").text.trim() actualCommitid = readCommitidFromJson() println "expected commitid from txt: ${expectedCommitid}" println "actual commitid from json: ${actualCommitid}" return expectedCommitid == actualCommitid } def readCommitidFromJson() { def workspacePath = pwd() def slurper = new JsonSlurper() def json = slurper.parseText(new File("${workspacePath}/info.json").text) def commitid = json.app.commitid return commitid }
I described the working of the script previously. There are three important constants that must match with our Jenkins installation:
- In the statement:
properties([[$class: 'GitLabConnectionProperty', gitLabConnection: 'my-gitlab-connection']])
‘my-gitlab-connection‘ matches the name I gave my gitlabConnection in the Jenkins plugin as I described here. - As I described before the ‘M3’ in the statement:
env.PATH = "${tool 'M3'}/bin:${env.PATH}"
must match the Maven installation in Jenkins as I described here. - Finally there is the line
mavenSettingsConfig: 'a1adf035-653b-410d-b5a6-16b6da77b322'
. The id mentioned here is the one copied from the settings file I set up with Config File Provider plugin as described here.
That’s all about the sources of the project. Let me show you next how to create the pipeline job in Jenkins. In the dashboard choose to create a new job of the type ‘pipeline’:
Next configure this job where the most important thing is to use the Jenkinsfile obtained from git. To configure this we have to use the username/password to log in into Gitlab (I haven’t found a way to use the Gitlab plugin here yet. You can also use another repo here if you want to keep your Jenkinsfiles separate from your project sources):
Now when I run the job it will fail at the last step with the following error:
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use new java.io.File java.lang.String
at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectNew(StaticWhitelist.java:187)
….
There is one final setting to do for this job to run successful. By default certain actions are not allowed by the pipeline job, so I have to tell Jenkins that in this case they are allowed.
To do this go to ‘Manage Jenkins’ and go to the ‘In-process Script Approval’:
There is a mention about a possible security vulnerability which you have to approve before the job will allow the action to happen:
After clicking the ‘Approve’ button en rerun the job there will be a second vulnerability which has to be approved for the job to finish successfully.
Now the build will be successful for all three stages as shown in the dashboard:
This concludes the example of continuous delivery and pipeline as code. As mentioned before this is just a very simple example of a pipeline but you could use it to get started with the concept and take much more out of it.
Reference: | Pipeline as code with a Spring Boot application from our JCG partner Pascal Alma at the The Pragmatic Integrator blog. |