One principle of Continuous Delivery which is often quoted is: "Everything is code".
"Everything..." wants to tell us that we should not only focus on our application code but also on things like
configuration, build instructions, tools, installation and setup routines.... everything which touches the software (or better: service) at least once in its lifetime should be treated as code.
"...is code" relates to the way how we treat code. Good ways of dealing with code is to put it under version control.
Write automated tests for the code, maybe do pair programming and/or have any sort of review process.
Basically all the things you (hopefully) already do with your application source code.
One way of putting your infrastructure in code is to use Chef. Chef is a configuration management tool which uses its own domain specific language (DSL) and ruby to express how to do certain tasks related to e.g. setup and installation of your software.
Chef is quiet mature in what it is doing and there are several test and quality tools that support you in the idea of "Everything is code".
One of these tools is Foodcritic, a lint tool for Chef code.
As we run all kind of static code checks/lint tools frequently on our Jenkins CI servers for our application code, I thought it would be a nice to apply the same to our Chef code.
To my surprise there was no Jenkins plug-in for Foodcritic, yet. (Yes, I found the holy grail! Finally a task for which there is no Jenkins plug-in yet!).
So I searched around and found this great blog by Eric G. Wolfe (@atomic_penguin) who uses Jenkins Warnings Plugin to include Rubocup into Jenkins.
So I applied the same idea to Foodcritic:
What we need:
* 1 Jenkins server
* 1 Warnings Plugin
* 1 Foodcritic
* 1 or more Chef cookbooks to check
Instructions:
- Install the Warnings Plugin on your Jenkins server.
- Install the Foodcritic ruby gem on your Jenkins machine.
(This requires your to install ruby (and gem) up-front, which I don't cover in this post. Just that much: If Foodcritic tells you that it requires ruby 2.x (and you only got 1.9.x) you can specify a version in the gem command with -v. E.g. gem foodcritic -v 4.0.0 installs Foodcritic 4.0.0 which runs with ruby 1.9.x)
- In your Jenkins configure an own compiler warnings parser for Foodcritic: "Manage Jenkins" -> "Configure System" -> "Compiler warnings"
Name:
Foodcritic
Link name:
Foodcritic warnings
Trend report name:
Foodcritic Lint Warnings
Regular Expression:
^(FC\d+): ([^:]+): ([^:]+):(\d+)$
Mapping Script:
import hudson.plugins.warnings.parser.Warning
import hudson.plugins.analysis.util.model.Priority
String ruleNumber = matcher.group(1)
String message = matcher.group(2)
String fileName = matcher.group(3)
String lineNumber = matcher.group(4)
Priority prio = Priority.NORMAL
String type = ""
if( ruleNumber == "FC030 ") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "annoyances, "
}
if( ruleNumber == "FC001" || ruleNumber == "FC019" || ruleNumber == "FC046" || ruleNumber == "FC047" ) {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "attributes, "
}
if( ruleNumber == "FC047" ) {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "chef11, "
}
if( ruleNumber == "FC045" ) {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "chef12, "
}
if( ruleNumber == "FC006" || ruleNumber == "FC007" || ruleNumber == "FC009" || ruleNumber == "FC010" || ruleNumber == "FC016" || ruleNumber == "FC017" || ruleNumber == "FC021" || ruleNumber == "FC022" || ruleNumber == "FC026" || ruleNumber == "FC027" || ruleNumber == "FC028" || ruleNumber == "FC029" || ruleNumber == "FC031" || ruleNumber == "FC032" || ruleNumber == "FC033" || ruleNumber == "FC034" || ruleNumber == "FC037" || ruleNumber == "FC038" || ruleNumber == "FC039" || ruleNumber == "FC045" || ruleNumber == "FC046" || ruleNumber == "FC047" || ruleNumber == "FC050" || ruleNumber == "FC051" || ruleNumber == "FC055" || ruleNumber == "FC056" || ruleNumber == "FC057" || ruleNumber == "FC058" || ruleNumber == "FC059" || ruleNumber == "FC060" || ruleNumber == "FC061") {
prio = Priority.HIGH
type += "correctness, "
}
if( ruleNumber == "FC015" ) {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "definitions, "
}
if( ruleNumber == "FC018" || ruleNumber == "FC025" || ruleNumber == "FC042" || ruleNumber == "FC043") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "deprecated, "
}
if( ruleNumber == "FC050") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "environments, "
}
if( ruleNumber == "FC040" || ruleNumber == "FC041") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "etsy, "
}
if( ruleNumber == "FC006" || ruleNumber == "FC013") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "files, "
}
if( ruleNumber == "FC014") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "libraries, "
}
if( ruleNumber == "FC015" || ruleNumber == "FC016" || ruleNumber == "FC017" || ruleNumber == "FC018" || ruleNumber == "FC021") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "lwrp, "
}
if( ruleNumber == "FC007" || ruleNumber == "FC008" || ruleNumber == "FC029" || ruleNumber == "FC031" || ruleNumber == "FC045" || ruleNumber == "FC052" || ruleNumber == "FC053" || ruleNumber == "FC055" || ruleNumber == "FC056" || ruleNumber == "FC061" || ruleNumber == "FC062" || ruleNumber == "FC063" || ruleNumber == "FC064" || ruleNumber == "FC065" ) {
prio = Priority.HIGH
type += "metadata, "
}
if( ruleNumber == "FC032" || ruleNumber == "FC043") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "notifications, "
}
if( ruleNumber == "FC003" || ruleNumber == "FC024") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "portability, "
}
if( ruleNumber == "FC048") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "processes, "
}
if( ruleNumber == "FC011" || ruleNumber == "FC012") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "readme, "
}
if( ruleNumber == "FC040" || ruleNumber == "FC041") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "recipe, "
}
if( ruleNumber == "FC049" || ruleNumber == "FC050") {
prio = Priority.HIGH
type += "roles, "
}
if( ruleNumber == "FC010") {
prio = Priority.HIGH
type += "search, "
}
if( ruleNumber == "FC004") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "services, "
}
if( ruleNumber == "FC003") {
prio = Priority.HIGH
type += "solo, "
}
if( ruleNumber == "FC002") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "strings, "
}
if( ruleNumber == "FC001" || ruleNumber == "FC002" || ruleNumber == "FC004" || ruleNumber == "FC005" || ruleNumber == "FC008" || ruleNumber == "FC011" || ruleNumber == "FC012" || ruleNumber == "FC013" || ruleNumber == "FC014" || ruleNumber == "FC015" || ruleNumber == "FC018" || ruleNumber == "FC019" || ruleNumber == "FC023" || ruleNumber == "FC025" || ruleNumber == "FC040" || ruleNumber == "FC041" || ruleNumber == "FC043" || ruleNumber == "FC044" || ruleNumber == "FC048" || ruleNumber == "FC049" || ruleNumber == "FC052" || ruleNumber == "FC053") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "style, "
}
if( ruleNumber == "FC064" || ruleNumber == "FC065") {
if( prio != Priority.HIGH ) {
prio = Priority.NORMAL
}
type += "supermarket, "
}
type = type.substring(0, type.length()-2)
return new Warning(fileName, Integer.parseInt(lineNumber), type, ruleNumber, message, prio);
Example Log Message:
FC017: Execute resource used to run curl or wget commands: /opt/jenkins/jobs/MyCookbook/workspace/recipes/myRecipe.rb:9
The script is for sure not perfect (basically its my first Groovy script) but it'll work. (You're free to change it and e.g. adjust the priorities to your needs)
What this does is create a new parser for console log messages that follow the message pattern given in the "Example Log Message" field. For each line the mapping script is called and it'll extract all relevant information out of the warning message given by Foodcritic.
- Create a Jenkins job to check your Chef cookbook
The job should sync down your Chef Cookbook (because you put it under version control, right?) and than just execute Foodcritic on it (as a Shell build step) like this:
/usr/bin/foodcritic ${WORKSPACE}
Finally you need to tell Jenkins that it should apply our new Foodcritic warnings parser on the console log as a post build step:
Enjoy your meal!