So some of you may know Cake and maybe even some of you may know Octopus. This post will try to setup a deployment and delivery environment like Octopus with only using Cake. Well to be honest, of course we will not create the full capabilities of Octopus. We will just try to archive the following goals.

  • single git repository
  • contains deployment logic
  • contains delivery logic
  • self contained

So what we are actually trying to archive is to create the kraken with Cake. Meaning that we have multiple tentacles that can be called to deploy something for us.

So why? Well let’s assume that you have some cross domain setup or you have multiple deployment target all over the world and you just don’t want to use Octopus. Say, you want to have control over the hole process. Say you want only rely on open source software.

Creating the repository

So our first goal is to create a new git repository that will eventually host our deployment tentacle. Let’s start by creating a new repository and adding the Cake bootstrapper. We open up PowerShell and type the following commands.

$> git init tentacle; cd tentacle
$> Invoke-WebRequest http://cakebuild.net/download/bootstrapper/windows -OutFile run.ps1

After we downloaded the bootstrapper we need to open it in our favorite text editor and change the first parameter definition because we named out bootstrapper run.ps1 and thus we will also name our cake file run.cake. So have a look for the following line.

[string]$Script = "build.cake"

Then rename the default value.

[string]$Script = "run.cake"

Add deployment target

Now we will create a new file named run.cake. We will start with something easy for the deployment here to make a point. The script could of course contain pretty much anything.

var target = Argument("target", "Default");

Task("Default") .Does(() => {
  DirectoryPath destination = Argument<string>("destination");
  EnsureDirectoryExists("./input");
  EnsureDirectoryExists(destination);
  CopyFiles(GetFiles("./input/**/*"), destination, true);
});

RunTarget(target);

Let’s have a look at what we just added. The default target of the file run.cake will always require the destination argument. Otherwise it will fail. That will give us some validation. The content of the input folder will then be copied into that given (local) destination. But somehow we need to add something to the input folder first, don’t we?

Add Download target

Let’s add another target.

Task("Retrieve") .Does(() => {
  var sources = Argument<string>("sources");
  var urls = sources.Split(';');
  EnsureDirectoryExists("./input");
  CleanDirectory("./input");
  foreach(var url in urls)
  {
    var file = url.Substring(url.LastIndexOf("/"));
    DownloadFile(url, "./input/" + file);
  }
});

So now we have a target that will download a set of files into the ./input folder. This target can easily be extended so that it might check if the file is a zip to perform extractions. What is now missing is something that will orchestrate our two targets together. Let’s add it!

Add web listener

We will use NancyFX to add a simple web application to our Cake file. That application will accept new deployments by web calls and will call Cake to perform them. Since NancyFX is not part of Cake we need to tell Cake to load some additional packages. We can do that by adding #addin directives at the beginning of our run.cake.

// Load Nancy assemblies into cake
#addin Nancy
#addin Nancy.Hosting.Self

Now we can implement our listener. Let’s add three new files for that.

  • ./listener/model.cake
  • ./listener/module.cake
  • ./listener/bootstrapper.cake

The ./listener/model.cake will contain our request payload. This should be pretty straight forward.

public class Payload
{
  public string[] Sources { get; set; }
  public string Destination { get; set; }
}

The ./listener/module.cake will contain our NancyModule that will add a HTTP POST route to the url /run. Meaning that it will expect some post content when that route is called. We will bind against the model we just created.

using Nancy;
using Nancy.ModelBinding;

public class ListenerModule : NancyModule
{
  private ICakeContext _cakeContext;

  public ListenerModule(ICakeContext cakeContext)
  {
    _cakeContext = cakeContext;

    Post["/run"] = _ => {
      var payload = this.Bind<Payload>();

      // Download sources
      var retrivalSettings = new CakeSettings();
      retrivalSettings.Arguments = new Dictionary<string, string>();
      retrivalSettings.Arguments["target"] = "retrieve";
      retrivalSettings.Arguments["sources"] = string.Join(";", payload.Sources);
      _cakeContext.CakeExecuteScript("./run.cake", retrivalSettings);

      // Run deployment
      var runSettings = new CakeSettings();
      runSettings.Arguments = new Dictionary<string, string>();
      runSettings.Arguments["destination"] = payload.Destination;
      _cakeContext.CakeExecuteScript("./run.cake", runSettings);

      // Return Ok
      return 200;
    };
  }
}

So at its core the module just looks into the given data and then calls two Cake targets. First we will call the Retrieve target and then the Default target.

Because the ICakeContext is not known to NancyFX by default we need to add a custom bootstrapper that will inject the current context into our web listener. We will add the bootstrapper to ./listener/bootstrapper.cake.

using Nancy.TinyIoc;
using Nancy.Bootstrapper;

public class Bootstrapper : DefaultNancyBootstrapper
{
  private ModuleRegistration[] modules;
  private ICakeContext _cakeContext;

  public Bootstrapper(ICakeContext cakeContext)
  {
    _cakeContext = cakeContext;
  }

  protected override IEnumerable<ModuleRegistration> Modules
  {
    get { return modules ?? (modules = new []{ new ModuleRegistration(typeof(ListenerModule)) } ); }
  }

  protected override void ConfigureApplicationContainer(TinyIoCContainer container)
  {
    base.ConfigureApplicationContainer(container);
    container.Register<ICakeContext>(_cakeContext);
  }
}

With the #load preprocessor directive we can tell Cake to load those files before compilation. Make sure to add the #load lines after the #addin lines.

// Load our listener code
#load listener/model.cake
#load listener/module.cake
#load listener/bootstrapper.cake

Add listener target

The only thing left to do now is to create another target that will start our new integrated web listener. Let’s call it StartListener

Task("StartListener") .Does(() => {
  var url = Argument<string>("url");
  var reservations = new UrlReservations{CreateAutomatically = true};
  var configuration = new HostConfiguration{UrlReservations = reservations};
  var bootstrapper = new Bootstrapper(Context);

  using(var host = new NancyHost(bootstrapper, configuration, new Uri(url))) {
    host.Start();
    Console.WriteLine("Running on " + url);
    Console.ReadLine();
  }
});

See it in action

Let’s look on what we just did. We created a new repository. We added a Cake bootstrapper. We then added a simple deployment and a simple download target. After that we integrated NancyFx to start a simple web server to receive deployments. Everything in one Cake file (with logical file separation for the listener). To give our new deployment repository a spin, we just clone it on a target machine and call our script via PowerShell.

.\run.ps1 -target StartListener --url=http://server.com:13121

Now everything left to do is to issue a post request to http://server.com:13121/run that contains a representation of our model inside the ./listener/model.cake. Following an example request.

{
  "destination": "C:\\Awesome\\",
  "sources":[
    "http://cakebuild.net/download/bootstrapper/windows"
  ]
}

We can post this quite easily with PowerShell. Just open up another window (remember, we just started our listener in the other one).

@{destination="C:\\Awesome\\";sources=@("http://cakebuild.net/download/bootstrapper/windows")}|ConvertTo-Json|Invoke-RestMethod -Uri "http://server.com:13121/run" -Method Post -ContentType "application/json" -UseBasicParsing

Recap

So now we have one self-contained repository containing our deployment target. And the extra bonus is an easy to run web API that can be used to send deployment data to our tentacle. Isn’t that cool?