Cake Build Tool

I don’t know exactly when or where I first came across the Cake build tool, and at the time I made a mental note to look at it in more detail (as I am not a massive fan of MSBuild). That time came and went, and I did nothing about it. Then Cake came across my radar again so this time I decided to dig into it a bit more.

 

So what is this cake build tool?

The Cake build tool is a build tool that utilizes the Roslyn (compiler as a service) from .NET. What this means is that you can write very precise build scripts using very familiar C# language syntax that you know and love.

 

Getting started

The best way to get started is to clone the example repo : https://github.com/cake-build/example

The repo is a simple C# class library and a test project all within a single solution.

 

image

As you can see this project is very simple. What we would like to do with this project is the following things :

  • Clean solution
  • Restore Nugets
  • Build solution
  • Run tests
  • And also have ability to push out Nuget package (nupkg file)

Most of this is already available within the example repo : https://github.com/cake-build/example, with the exception of pushing a nuget package at the end.

 

What bits do you need to run a cake build?

So what do you need to provide to run a cake build

You just need these 2 files

  • build.ps1 (bootstrapper that doesn’t change, grab it from repo example above)
  • build. cake (this is your specific build and should contain the targets/tasks you need for your build)

 

The .cake file

As the build.ps1 is a standard thing I won’t worry about that, but lets now turn our attention to the build.cake file which for this post looks like this

 

#tool nuget:?package=NUnit.ConsoleRunner&version=3.4.0


//////////////////////////////////////////////////////////////////////
// ARGUMENTS
//////////////////////////////////////////////////////////////////////

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

//////////////////////////////////////////////////////////////////////
// PREPARATION
//////////////////////////////////////////////////////////////////////

// Define directories.
var buildDir = Directory("./src/Example/bin") + Directory(configuration);

//////////////////////////////////////////////////////////////////////
// TASKS
//////////////////////////////////////////////////////////////////////

Task("Clean")
    .Does(() =>
{
    CleanDirectory(buildDir);
});

Task("Restore-NuGet-Packages")
    .IsDependentOn("Clean")
    .Does(() =>
{
    NuGetRestore("./src/Example.sln");
});

Task("Build")
    .IsDependentOn("Restore-NuGet-Packages")
    .Does(() =>
{
    if(IsRunningOnWindows())
    {
      // Use MSBuild
      MSBuild("./src/Example.sln", settings =>
        settings.SetConfiguration(configuration));
    }
    else
    {
      // Use XBuild
      XBuild("./src/Example.sln", settings =>
        settings.SetConfiguration(configuration));
    }
});

Task("Run-Unit-Tests")
    .IsDependentOn("Build")
    .Does(() =>
{
    NUnit3("./src/**/bin/" + configuration + "/*.Tests.dll", new NUnit3Settings {
        NoResults = true
        });
});


var nugetPackageDir = Directory("./artifacts");
var nuGetPackSettings = new NuGetPackSettings
{   
  OutputDirectory = nugetPackageDir  
};

Task("Package")
  .Does(() => NuGetPack("./src/Example/Example.nuspec", nuGetPackSettings));


//////////////////////////////////////////////////////////////////////
// TASK TARGETS
//////////////////////////////////////////////////////////////////////

Task("Default")
    .IsDependentOn("Run-Unit-Tests");

//////////////////////////////////////////////////////////////////////
// EXECUTION
//////////////////////////////////////////////////////////////////////

RunTarget(target);

 

 

There are a couple of concepts to call out there

 

  • We have some top level arguments/ variables
  • Nice C# features that we have used before
  • We have Tasks just like other build systems. We can make one task depend on another using .IsDependantOn(“”)
  • There seems to be wide range of inbuilt things we can use for example these guys below. These are all prebuilt items in the cake DSL that we can make use of. There are loads of these, the full list is available here : https://cakebuild.net/dsl/
    • CleanDirectory
    • NUnit3
    • NuGetPack

 

Have a look at the DSL web site there are quite a few cool things you can use

 

image

 

Running the build

So with this build.cake and build.ps1 (bootstrapper file) in place we would like to run the build. Here is how we do that

 

1. Open PowerShell window as Administrator
2. Issue this command in PowerShell : Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
3. Change to the correct directory with the .cake file in it, and issue this command : .\build.ps1
4. You should see some output, where it eventually completes
5. You should also see a tools folder

 

This is the tail end of the build I just ran above

 

image

 

And this is the sort of thing that we should see in the tools folder that the cake build created

 

image

 

Deploying a Nuget

So I stated that I also wanted to be able to deploy a Nuget Package as a Nupkg. To do this I need to create the following .nuspec file for the Example project

<?xml version="1.0"?>
<package >
  <metadata>
    <id>Example</id>
    <version>1.0.0</version>
    <title>Cake Example</title>
    <authors>Sacha Barber</authors>
    <owners>Sacha Barber</owners>
    <licenseUrl>http://github.com/sachabarber</licenseUrl>
    <projectUrl>http://github.com/sachabarber</projectUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Simple Cake Build Tool Example</description>
    <releaseNotes>1st and only release</releaseNotes>
    <copyright>Copyright 2018</copyright>
    <tags>C# Cake</tags>
  </metadata>
  <files>  
   <file src="bin\Release\Example.dll" target="lib\net45"></file>  
</files> 
</package>

 

So with that in place we can also try the Nuget publish Task that our build.cake file has in it like this:

 

1. Open PowerShell window as Administrator
2. Issue this command in PowerShell : Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
3. Issue this command in PowerShell : .\build.ps1 -Target Package

 

After running that we should see artifacts folder with the following artifact in it

 

image

 

Conclusion

I was pretty happy with this, I went from not using Cake at all to carrying out ALL my requirements in 1 hour on a train ride with limited WiFi. It just seems to work, and I imagine it would be a good fit for working with something like https://about.gitlab.com/

 

I think I will be looking to use this little build tool a lot more.

Advertisements

Kubernetes – Part 2 of n, creating our first POD

So it has taken me a while to do this post,so apologies on that front. Anyway if you recall from the 1st article in this series of posts this was the rough agenda

 

  1. What is Kubernetes / Installing Minikube
  2. What are pods/labels, declaring your first pod  (this post)
  3. Services
  4. Singletons (such as a DB)
  5. ConfigMaps/Secrets
  6. LivenessProbe/ReadinessProbe/Scaling Deployments

 

 

So as you can see above this post will talk about PODs in Kubernetes. So lets jump straight in

 

What Is a POD?

Here is the official blurb from the Kubernetes web site

A pod (as in a pod of whales or pea pod) is a group of one or more containers (such as Docker containers), with shared storage/network, and a specification for how to run the containers. A pod’s contents are always co-located and co-scheduled, and run in a shared context. A pod models an application-specific “logical host” – it contains one or more application containers which are relatively tightly coupled — in a pre-container world, they would have executed on the same physical or virtual machine.

While Kubernetes supports more container runtimes than just Docker, Docker is the most commonly known runtime, and it helps to describe pods in Docker terms.

The shared context of a pod is a set of Linux namespaces, cgroups, and potentially other facets of isolation – the same things that isolate a Docker container. Within a pod’s context, the individual applications may have further sub-isolations applied.

Containers within a pod share an IP address and port space, and can find each other via localhost. They can also communicate with each other using standard inter-process communications like SystemV semaphores or POSIX shared memory. Containers in different pods have distinct IP addresses and can not communicate by IPC without special configuration. These containers usually communicate with each other via Pod IP addresses.

Applications within a pod also have access to shared volumes, which are defined as part of a pod and are made available to be mounted into each application’s filesystem.

In terms of Docker constructs, a pod is modelled as a group of Docker containers with shared namespaces and shared volumes.

Like individual application containers, pods are considered to be relatively ephemeral (rather than durable) entities. As discussed in life of a pod, pods are created, assigned a unique ID (UID), and scheduled to nodes where they remain until termination (according to restart policy) or deletion. If a node dies, the pods scheduled to that node are scheduled for deletion, after a timeout period. A given pod (as defined by a UID) is not “rescheduled” to a new node; instead, it can be replaced by an identical pod, with even the same name if desired, but with a new UID (see replication controller for more details). (In the future, a higher-level API may support pod migration.)

When something is said to have the same lifetime as a pod, such as a volume, that means that it exists as long as that pod (with that UID) exists. If that pod is deleted for any reason, even if an identical replacement is created, the related thing (e.g. volume) is also destroyed and created anew.

 

image

 

A multi-container pod that contains a file puller and a web server that uses a persistent volume for shared storage between the containers.

 

Taken from https://kubernetes.io/docs/concepts/workloads/pods/pod/#what-is-a-pod up on date 16/01/18

 

Ok so that’s the official low down. So what can we extract from the above paragraph that will help us understand a bit more about how to get what a POD is, and how we can create our own ones?

  • PODs can run one or more things (in containers)
  • It supports multiple container providers but everyone mainly uses Docker
  • PODs seem to be lowest building block in the Kubernetes echo-system

 

Alright, so now that we know that, we can get to work with some of this. What we can do is think up a simple demo app that would allow us to exercise some (though not all, you will have to learn some stuff on your own dime) of the Kubernetes features.

 

  • A simple web API is actually quite a good choice as it usually exposes a external façade that can be called (REST endpoint say), and it is also easy to use to demonstrate some more advanced Kubenetes topics such as
    • Services
    • Deployments
    • Replication Sets
    • Health Checks

 

The Service Stack REST API

So for this series of posts we will be working with a small Service Stack REST API that we will expand over time. For this post, the ServiceStack endpoint simple allows this single route

  • Simple GET : http:[IP_ADD]:5000/hello/{SomeStringValueOfYourChoice}

 

In that route the [IP_ADD] is of much interest. This will ultimately be coming from Kubenetes. Which will get to by the end of this post.

 

Where Is It’s Code?

The code for this one will be available here : https://github.com/sachabarber/KubernetesExamples/tree/master/Post2_SimpleServiceStackPod/sswebapp

 

I think my rough plan at this moment in time is to create a new folder for each post, even though the underlying code base will not be changing that much. That way we can create a new Docker image from each posts code quite easily where we can tag it with a version and either push it DockerHub or a private docker repository (we will talk about this in more detail later)

 

For now just understand that one post = one folder in git, and this will probably end up being 1 tagged verion of a Docker image (if you don’t know what that means don’t worry we will cover more of that later too)

 

 

So What Does The ServiceStack API Look Like?

 

Well it is a standard ServiceStack .NET Core API project (which I created using the ServiceStack CLI tools). The rough shape of it is as follows

 

image

 

  • sswebapp = The actual app
  • sswebapp.ServiceInterface = The service contract
  • sswebapp.ServiceModel = The shared contracts
  • sswebapp.Tests = Some simple tests

 

I don’t think there is that much merit in walking through all this code. I guess the only one call out I would make with ServiceStack is that it uses a Message based approach rather than a traditional URL/Route based approach. You can still have routing but it’s a secondary concern that is overriden by the type of message being the real decided in what code gets called based on the payload sent.

 

For this posts demo app this is the only available route

 

using ServiceStack;

namespace sswebapp.ServiceModel
{
    [Route("/hello")]
    [Route("/hello/{Name}")]
    public class Hello : IReturn<HelloResponse>
    {
        public string Name { get; set; }
    }

    public class HelloResponse
    {
        public string Result { get; set; }
    }
}

 

This would equate to the following route GET : http:[IP_ADD]:5000/hello/{SomeStringValueOfYourChoice} where the {SomeStringValueOfYourChoice} would be fed into the Name property of the Hello object shown above

 

The Docker File

Obviously since we know we need an image for Kubernetes to work properly, we need to create one. As we now know Kubernetes can work with many different container providers, but it does has a bias towards Docker. So we need to Docker’ize the above .NET Core Service Stack API example. How do we do that?

 

Well that part is actually quite simple, we just need to create a Docker file. So without further ado lets have a look at the Dockerfile for this demo code above

 

FROM microsoft/aspnetcore-build:2.0 AS build-env
COPY src /app
WORKDIR /app

RUN dotnet restore --configfile ./NuGet.Config
RUN dotnet publish -c Release -o out

# Build runtime image
FROM microsoft/aspnetcore:2.0
WORKDIR /app
COPY --from=build-env /app/sswebapp/out .
ENV ASPNETCORE_URLS http://*:5000
ENTRYPOINT ["dotnet", "sswebapp.dll"]

 

These are main points from the above:

  • We use microsoft/aspnetcore-build:2.0 as the base image
  • We are then able to use the dotnet command to do a few things
  • We then bring in another later microsoft/aspnetcore
  • Before finally adding our own code as the final layer for Docker
  • We then specify port (annpyingly the Kestrel webserver that comes with .NET Core is only port 5000, which is also by some strange act of fate the port that a Docker private repo wants to use….but more on this later), for now we just want to expose the port and specify the start up entry point

 

 

 

MiniKube Setup Using DockerHub

 

For this section I am using the most friction free way of testing out minikube + Docker images. I am using Docker Cloud to host my repo/images. This is the workflow for this section

 

image

 

Image taken from https://blog.hasura.io/sharing-a-local-registry-for-minikube-37c7240d0615 up on date 19/02/18

 

The obvioulsy issue here is that we have a bit of software locally we want to package up into a Docker image and use in MiniKube which is also on our local box. However the Docker daemon in MiniKube is not the same one as outside of MiniKube. Remember MiniKube is in effect a VM that just runs headless. There is also more complication where by MiniKube will want to try and pull images, and may require security credentials. We can work around with this by creating a private docker repo (which I will not use in this series but do talk about below). The article linked above and the other one which I mention at the bottom are MUST reads if you want to do that with MiniKube. I did get it working, but however opted for a simple life and will be using DockerHub to store all my images/repos for this article series.

 

Ok now that we have a DockerFile and we have decided to use DockerHub to host the repo/image, how do we get this to work in Kubernetes?

 

Pushing To DockerHub

So the first thing you will need to do is create a DockerHub account, and then create a PUBLIC repo. For me the repo was called “sswebapp” and my DockerHub user is ”sachabarber”. So this is what it looks like in DockerHub after creating the repo

 

image

 

Ok with that now in place we need to get the actual Docker image up to DockerHub. How do we do that part?

These are the steps (obviously your paths may be different)

docker login --username=sachabarber
cd C:\Users\sacha\Desktop\KubernetesExamples\Post2_SimpleServiceStackPod\sswebapp
docker build -t "sswebapp:v1" .
docker tag sswebapp:v1 sachabarber/sswebapp:v1
docker push sachabarber/sswebapp

 

Ok so with now in place all we need to do is take care of the Kubernetes side of things now

 

Running A DockerHub Image In Kubernetes

So we now have a DockerHub image available, we now need to get Kubernetes to use that image. With Kubenetes there is a basic set of Kubectl commands that cover most of the basics, and then if that is not good enough you can specify most things in YAML files.

 

We will start out with Kubectl commands and then have a look at what the equivalent YAML would have been

 

So this is how we can create a POD which must be exposed via something called a service, which for now just trust me you need. We will be getting on to these in a future post.

 

c:\
cd\
minikube.exe start --kubernetes-version="v1.9.0" --vm-driver="hyperv" --memory=1024 --hyperv-virtual-switch="Minikube Switch" --v=7 --alsologtostderr 
kubectl run simple-sswebapi-pod-v1 --replicas=1 --labels="run=sswebapi-pod-v1" --image=sachabarber/sswebapp:v1  --port=5000
kubectl expose deployment simple-sswebapi-pod-v1 --type=NodePort --name=simple-sswebapi-service
kubectl get services simple-sswebapi-service
minikube service simple-sswebapi-service --url 

 

So what exactly is going on in there? Well there are a few things of note:

  • We are starting minikube up
  • We use Kubectl to run a new deployment (this is our POD that makes use of our DockerHub image) and we also expose a port at this time
  • We use Kubectl to expose the deployment via a service (future posts will cover this)
  • We then get our new service grab the external Url from it using the “—url” flag, and then we can try it in a browser

 

What Would All This Look Like In YAML?

So above we saw 2 lines that create the deployment and one that creates a service. I also mentioned that the Kubctl.exe command line will get you most of the way there for basics, but for more sophisticated stuff we need to use YAML to describe the requirements.

 

Lets have a look at what the Deployment / Service would look like in YAML.

 

Here is the Deployment

using command line

kubectl run simple-sswebapi-pod-v1 --replicas=1 --labels="run=sswebapi-pod-v1" --image=sachabarber/sswebapp:v1  --port=5000

 

And here is the YAML equivalent

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: simple-sswebapi-pod-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: run=sswebapi-pod-v1
    spec:
      containers:
      - name: sswebapi-pod-v1
        image: sachabarber/sswebapp:v1
        ports:
        - containerPort: 5000

 

 

Here is the Service

using command line

kubectl expose deployment simple-sswebapi-pod-v1 --type=NodePort --name=simple-sswebapi-service

And here is the YAML equivalent

apiVersion: v1
kind: Service
metadata:
  name: simple-sswebapi-service
spec:
  selector:
    app: run=sswebapi-pod-v1
  ports:
  - protocol: TCP
    port: 5000
    targetPort: 5000
  type: NodePort

 

 

When you use YAML files these must be applied as follows:

kubectl apply -f <FILENAME>

 

Now that we have all the stuff in place, and deployed we should be able to try things out. Lets do that now.

 

Importance Of Labels

Labels in Kubernetes play a vital role, in that they allow other higher level abstractions, to quickly locate PODs for things like

  • Exposing via a service
  • Routing
  • Replica sets checks
  • Health checks
  • Rolling upgrades

 

All of these higher level abstractions are looking for things based on a  particular version. Labels also come with selector support, that allows Kubernetes to identify the right PODs for an action. This is an important concept are you would do well to read the official docs on this : https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/

 

 

 

 

Pod in dashboard

If we ran the command c:\minikube dashboard, and moved to the Pods section we should now see this

 

image

 

Service in dashboard

If we ran the command c:\minikube dashboard, and moved to the Services section we should now see this

 

image

 

Testing the endpoint from a browser

If we ran the command c:\minikube service simple-sswebapi-service –url, and took a note of whatever IP address it gave us we can test the deployment via a browser windows, something like the following

 

image

 

 

Declarative Nature Of Kubernetes

One of the best things about Kubenetes in my opinion is that is is declarative in nature, not imperative. This is great as I can just say things like replicas: 4. I don’t have to do anything else and Kubernetes will just ensure that this agreement is met. We will see more of this in later posts, but for now just realise that the way Kubernetes work is using a declarative set of requirements.

 

MiniKube Setup Using A Private Repository

 

This workflow  will setup a private Docker repository on port 5000, that will be used by MiniKube. This obviously saved the full round trip to Docker Cloud.

image

 

Image taken from https://blog.hasura.io/sharing-a-local-registry-for-minikube-37c7240d0615 up on date 19/02/18

 

Although its slightly out of scope for this post this section shows you how you should be able to host a private Docker repository in the Docker daemon that lives inside the MiniKube VM that we setup in post 1. Luckily Docker allows its own registry for images to be run as a container using this image : https://hub.docker.com/_/registry/

 

Which allows you to run a private repository on port 5000

 

docker run -d -p 5000:5000 --restart always --name registry registry:2

This should then allow you to do things like this

docker pull ubuntu
docker tag ubuntu localhost:5000/ubuntu
docker push localhost:5000/ubuntu

 

This obviously saves you the full round trip from your PC (Docker Daemon) –> Cloud (Docker repo) –> your PC (MiniKube)

As its now more like this your PC (Docker Daemon) –> your PC (Docker repo)–> your PC (MiniKube) thanks to the local private repo

 

 

The idea is that you would do something like this

 

NOTE : that the 5000 port is also the default one used by the .NET Core Kestrel http listener, so we would need to adjust the port in the Dockerfile for this article, and how we apply the Docker file into Kubernetes to use a different port from 5000, but for now lets carry on with how we might setup a private Docker repo)

 

in PowerShell

c:\
cd\
minikube.exe start --kubernetes-version="v1.9.0" --vm-driver="hyperv" --memory=1024 --hyperv-virtual-switch="Minikube Switch" --v=7 --alsologtostderr --insecure-registry localhost:5000
minikube docker-env
& minikube docker-env | Invoke-Expression
kubectl.exe apply -f C:\Users\sacha\Desktop\KubernetesExamples\Post2_SimpleServiceStackPod\sswebapp\LocalRegistry.yaml

 

Then in a Bash shell

kubectl port-forward --namespace kube-system \
$(kubectl get po -n kube-system | grep kube-registry-v0 | \
awk '{print $1;}') 5000:5000

 

Then back into PowerShell

cd C:\Users\sacha\Desktop\KubernetesExamples\Post2_SimpleServiceStackPod\sswebapp
docker build -t "sswebapp:v1" .
docker tag sacha/sswebapp:v1 localhost:5000/sacha/sswebapp:v1
docker push localhost:5000/sacha/sswebapp:v1
kubectl run simple-sswebapi-pod-v1 --replicas=1 --labels="run=sswebapi-pod-v1" --image=localhost:5000/sacha/sswebapp:v1  --port=5000
kubectl expose deployment simple-sswebapi-pod-v1 --type=NodePort --name=simple-sswebapi-service
kubectl get services simple-sswebapi-service
minikube service simple-sswebapi-service --url 

Obviously you will need replace bits of the above with you own images/paths, but that is the basic idea.

 

If you cant follow this set of instructions you can try these 2 very good articles on this :

 

 

 

Word Of Warning About Using MiniKube  For Development

Minikube ONLY supports Docker Linux Containers so make sure you have set Docker to use that NOT Windows Containers. You can do this from the Docker system tray icon.

Kubernetes – Part 1 of n, Installing MiniKube

So at the moment I am doing a few things, such as

 

  • Reading a good Scala book
  • Reading another book on CATS type level programming for Scala
  • Looking into Azure Batch
  • Deciding whether I am going to make myself learn GoLang (which I probably will)

 

Amongst all of that I have also decided that I am going to obligate myself to writing a small series of posts on Kubernetes. The rough guide of the series might be something like shown below

 

  1. What is Kubernetes / Installing Minikube (this post)
  2. What are pods/labels, declaring your first pod
  3. Services
  4. Singletons (such as a DB)
  5. ConfigMaps/Secrets
  6. LivenessProbe/ReadinessProbe/Scaling Deployments

 

So yeah that is the rough guide of what I will be doing. I will most likely condense all of this into a single www.codeproject.com article at the end too, as I find there is a slightly different audience for articles than there is for blob posts.

 

So what is kubernetes?

Kubernetes (The name Kubernetes originates from Greek, meaning helmsman or pilot, and is the root of governor and cybernetic) is an open-source system for automating deployment, scaling, and management of containerized applications.

It groups containers that make up an application into logical units for easy management and discovery. Kubernetes builds upon 15 years of experience of running production workloads at Google, combined with best-of-breed ideas and practices from the community

 

Kubernetes builds upon previous ventures by Google such as Borg and Omega, but it also uses the current container darling Docker, and is a free tool.

 

Kubernetes can run in a variety of ways,  such as

  • Managed cloud service (AWS and Azure both have container services that support Kubernetes out of the box)
  • On bare metal where you have a cluster of virtual machines (VMs) that you will install Kubernetes on (see here for really good guide on this)
  • Minikube – Running a very simple SINGLE node cluster on your own computer (I will be using this for this series just for its simplicity and cost savings)

 

So without further ado we will be starting this series of with a simple introduction, of how to install Kubernetes locally using Minikube.

 

Installing Minikube

I am using a Windows PC, so these instructions are biased towards Windows development where we will be using Hyper-V instead of VirtualBox. But if you prefer to use VirtualBox I am sure you can find out how to do the specific bits that I talk about below for Hyper-V in VirtualBox

 

Ok so lets get started.

 

Installing Docker

 

The first thing you will need to do is grab Docker from here (I went with the stable channel). So download and install that. This should be a fairly vanilla install. At the end you can check the installation using 2 methods

 

Checking your system tray Docker icon

 

image

 

And trying a simple command in PowerShell (if you are using Windows)

 

image

 

Ok so now that Docker looks good, lets turn our attention to Hyper-V. As I say you could use VirtualBox, but since I am using Windows, Hyper-V just seems a better more integrated choice. So lets make sure that is turned on.

 

 

Setup Hyper-V

Launch Control Panel –> Programs and Features

 

image

 

Then we want to ensure that Hyper-V is turned on, we do this by using the “Turn Windows features on or off”, and then finding Hyper-V and checking the relevant checkboxes

 

image

 

Ok so now that you have Hyper-V enabled we need to launch Hyper-V Manager and add a new Virtual Switch (we will use this Switch name later when we run Minikube). We need to add a new switch to provide isolation from the Virtual Switch that Docker sets up when it installs.

image

 

So once Hyper-V Manager launches, create a new “External” Virtual Switch

 

image

 

Which you will need to configure like this

 

image

 

Installing Minikube

Ok now what we need to do is grab the minikube binary from github. The current releases are maintained here : https://github.com/kubernetes/minikube/releases

You will want to grab the one called minikube-windows-amd64 as this blog is a Windows installation guide. Once downloaded you MUST copy this file to the root of C:\. This needs to be done due a known bug (read  more about it here : https://github.com/kubernetes/minikube/issues/459).

 

Ok so just for you own sanity rename the file c:\minikube-windows-amd64 to c:\minikube.exe for brevity when running commands.

 

Installing kubectrl.exe

Next you will need to download kubectrl.exe which you can do by using a link like this, where you would fill the link with the version you want. For this series I will be using v1.9.0 so my link address is : http://storage.googleapis.com/kubernetes-release/release/v1.9.0/bin/windows/amd64/kubectl.exe Take this kubectrl.exe and place it alongside you minikube.exe in C:\

 

Provisioning the cluster

Ok so now that we have the basic setup, and required files, we need to test our installation. But before that it is good to have a look at the minikube.exe commands/args which are all documented via a command like this which you can run in PowerShell

 

image

 

The actual command we are going to use it as follows

.\minikube.exe start --kubernetes-version="v1.9.0" --vm-driver="hyperv" --memory=1024 --hyperv-virtual-switch="Minikube Switch" --v=7 --alsologtostderr

 

You may be wondering where some of these values come from. Well I have to admit it is not that clear from the command line –help text you see above. You see above. You do have to dig a bit. perhaps the most intriguing ones above are

  • vm-driver
  • hyperv-virtual-switch

 

These instruct minikube to use HyperV and also to use the new HyperV Manager switch we set up above.

Make sure you get the name right. It should match the one you setup

 

You can read more about the HyperV command args here  : https://github.com/kubernetes/minikube/blob/master/docs/drivers.md#hyperV-driver

 

Anyway lets get back to business where we run this command line (I am using PowerShell in Administrator mode), we should see some output like this, where it eventually ends up with some like this

image

 

This does a few things for you behind the scenes

  • Creates a Docker Vm which is run in HyperV for you
  • The host is provisioned with boot2docker.iso and set up
  • It configures kubectrl.exe to use the local cluster

 

Checking Status

You can check on the status of the cluster using the following command like

 

image

 

Stale Context

If you see this sort of thing

image

You can fix this like this:

image

 

Verifying other aspects

The final task to ensure that the installation is sound is to try and view the cluster info and dashboard, like this:

 

image

 

This should bring up a web UI

image

 

So that is all looking good.

 

So that’s it for this post, I will start working on the next ones very soon….stay tuned

Small Azure EventGrid + Azure Functions Demo

I am a big fan of reactive programming, and love things like RX/Akka, and service buses and things like that, and I have been meaning to try the new (preview) Azure EventGrid service out for a while.

 

To this end I have given it a little go where I hooked it up to a Azure Function and written a small article about it here : https://www.codeproject.com/Articles/1220389/Azure-EventGrid-Azure-Function-demo

Azure Service Fabric Demo App

At work we have made use of the Azure Service Fabric, and I thought it might be nice to write up some of the fun we had with that. To this end I have written an article on it at codeproject.com which you can read here : https://www.codeproject.com/Articles/1217885/Azure-Service-Fabric-demo

The article covers :

  • Service Fabric basics
  • IOC
  • Logging (Serilog/Seq)
  • Encryption of connection strings

Anyway hope you like it

I’m going to write up this big Scala thing I have been doing, then I may post some more Azure bits and bobs, adios until then

 

 

MADCAP IDEA 13 : GETTING THE ‘VIEW JOB’ TO WORK END-END

 

Last Time

Last time we looked at finishing the “Create Job” page, which creates the initial Job and sends it out via Kafka through the play backend and back to other users browsers via comet/websock and finally some RX.js. This post will see us finish the entire system by implementing the final page “ViewJob”

 

PreAmble

Just as a reminder this is part of my ongoing set of posts which I talk about here :

https://sachabarbs.wordpress.com/2017/05/01/madcap-idea/, where we will be building up to a point where we have a full app using lots of different stuff, such as these

 

  • WebPack
  • React.js
  • React Router
  • TypeScript
  • Babel.js
  • Akka
  • Scala
  • Play (Scala Http Stack)
  • MySql
  • SBT
  • Kafka
  • Kafka Streams

 

Ok so now that we have the introductions out of the way, lets crack on with what we want to cover in this post.

 

Where is the code?

As usual the code is on GitHub here : https://github.com/sachabarber/MadCapIdea

 

What Is This Post All About?

As stated above this post deals with the “View Job” functions

 

This is kind of a hard post to write, as what I will have to do really is just present a wall of text, as its all very isolated to one single typescript file with a couple of helper classes. However what might help is to talk about some of the actions that this wall of text that follows will do

 

Is This The End?

Whilst this does indeed wrap up the blog side of things, I intend to do a single  http://www.codeproject.com article which will be a highly compressed version of these 13 posts. As such it may be easier to get the gist of what I have done here then reading these 13 posts.

 

So what should the “View Job” page do?

This page should do the following things

  • If a passenger sends out a job it should be seen by ANY driver that is logged in (providing the job is not already assigned to a driver)
  • Positions updates from a passenger to drivers (that know about the passenger) should show the new passenger position
  • When a driver pushes out (single laptop requires that users click on map to make their own position known to others) their new position that the client sees that and updates the driver marker accordingly
  • That a passenger can accept a driver for a job
  • That a driver can not accept a job from a passenger
  • That once a job is paired between passenger/driver only those 2 markers will be shown if you are either of these users
  • That once a job is paired between passenger/driver AND YOU ARE NOT ONE OF THESE USERS that you ONLY see your own markers
  • That a job may be completed by passenger OR driver independently and that they are able to “Rate” each other

 

I think those are the main points. So how about that wall of text.

 

Wall of text that does the stuffz

 

Here is the wall of text

import * as React from "react";
import * as ReactDOM from "react-dom";
import * as _ from "lodash";
import Measure from 'react-measure'
import { RatingDialog } from "./components/RatingDialog";
import { YesNoDialog } from "./components/YesNoDialog";
import { OkDialog } from "./components/OkDialog";
import { AcceptList } from "./components/AcceptList";
import 'bootstrap/dist/css/bootstrap.css';
import {
    Well,
    Grid,
    Row,
    Col,
    ButtonInput,
    ButtonGroup,
    Button,
    Modal,
    Popover,
    Tooltip,
    OverlayTrigger
} from "react-bootstrap";
import { AuthService } from "./services/AuthService";
import { JobService } from "./services/JobService";
import { JobStreamService } from "./services/JobStreamService";
import { PositionService } from "./services/PositionService";
import { Position } from "./domain/Position";
import { PositionMarker } from "./domain/PositionMarker";
import { hashHistory } from 'react-router';
import { withGoogleMap, GoogleMap, Marker, OverlayView } from "react-google-maps";

const STYLES = {
    overlayView: {
        background: `white`,
        border: `1px solid #ccc`,
        padding: 15,
    }
}


const GetPixelPositionOffset = (width, height) => {
    return { x: -(width / 2), y: -(height / 2) };
}



const ViewJobGoogleMap = withGoogleMap(props => (

    <GoogleMap
        ref={props.onMapLoad}
        defaultZoom={16}
        defaultCenter={{ lat: 50.8202949, lng: -0.1406958 }}
        onClick={props.onMapClick}>
        {props.markers.map((marker, index) => (
            <OverlayView
                key={marker.key}
                mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
                position={marker.position}
                getPixelPositionOffset={GetPixelPositionOffset}>
                <div style={STYLES.overlayView}>
                    <img src={marker.icon} />
                    <strong>{marker.key}</strong>
                </div>
            </OverlayView>
        ))}
    </GoogleMap>
));


export interface ViewJobState {
    markers: Array<PositionMarker>;
    okDialogOpen: boolean;
    okDialogKey: number;
    okDialogHeaderText: string;
    okDialogBodyText: string;
    dimensions: {
        width: number,
        height: number
    },
    currentPosition: Position;
    isJobAccepted: boolean;
    finalActionHasBeenClicked: boolean;
}

type DoneCallback = (jdata: any, textStatus: any, jqXHR: any) => void


export class ViewJob extends React.Component<undefined, ViewJobState> {

    private _authService: AuthService;
    private _jobService: JobService;
    private _jobStreamService: JobStreamService;
    private _positionService: PositionService;
    private _subscription: any; 
    private _currentJobUUID: any;

    constructor(props: any) {
        super(props);
        this._authService = props.route.authService;
        this._jobStreamService = props.route.jobStreamService;
        this._jobService = props.route.jobService;
        this._positionService = props.route.positionService;
        
        if (!this._authService.isAuthenticated()) {
            hashHistory.push('/');
        }

        let savedMarkers: Array<PositionMarker> = new Array<PositionMarker>();
        if (this._positionService.hasJobPositions()) {
            savedMarkers = this._positionService.userJobPositions();
        }

        this.state = {
            markers: savedMarkers,
            okDialogHeaderText: '',
            okDialogBodyText: '',
            okDialogOpen: false,
            okDialogKey: 0,
            dimensions: { width: -1, height: -1 },
            currentPosition: this._authService.isDriver() ? null :
                this._positionService.currentPosition(),
            isJobAccepted: false,
            finalActionHasBeenClicked: false
        };
    }

    componentWillMount() {
        var self = this;
        this._subscription =
            this._jobStreamService.getJobStream()
            .retry()
            .where(function (x, idx, obs) {
                return self.shouldShowMarkerForJob(x.detail);
            })
            .subscribe(
                jobArgs => {

                    console.log('RX saw onJobChanged');
                    console.log('RX x = ', jobArgs.detail);

                    this._jobService.clearUserIssuedJob();
                    this._jobService.storeUserIssuedJob(jobArgs.detail);
                    this.addMarkerForJob(jobArgs.detail);
                },
                error => {
                    console.log('RX saw ERROR');
                    console.log('RX error = ', error);
                },
                () => {
                    console.log('RX saw COMPLETE');
                }
            );
    }

    componentWillUnmount() {
        this._subscription.dispose();
        this._positionService.storeUserJobPositions(this.state.markers);
    }

    render() {

        const adjustedwidth = this.state.dimensions.width;

        return (
            <Well className="outer-well">
                <Grid>
                    <Row className="show-grid">
                        <Col xs={10} md={6}>
                            <h4>CURRENT JOB</h4>
                        </Col>
                    </Row>
                    <Row className="show-grid">
                        <Col xs={10} md={6}>
                            <AcceptList
                                markers={_.filter(this.state.markers, { isDriverIcon: true })}
                                currentUserIsDriver={this._authService.isDriver()}
                                clickCallback={this.handleMarkerClick}
                            />
                        </Col>
                    </Row>
                    <Row className="show-grid">
                        <Col xs={10} md={6}>
                            <Measure
                                bounds
                                onResize={(contentRect) => {
                                    this.setState({ dimensions: contentRect.bounds })
                                }}>
                                {({ measureRef }) =>
                                    <div ref={measureRef}>
                                        <ViewJobGoogleMap
                                            containerElement={
                                                <div style={{
                                                    position: 'relative',
                                                    top: 0,
                                                    left: 0,
                                                    right: 0,
                                                    bottom: 0,
                                                    width: { adjustedwidth },
                                                    height: 600,
                                                    justifyContent: 'flex-end',
                                                    alignItems: 'center',
                                                    marginTop: 20,
                                                    marginLeft: 0,
                                                    marginRight: 0,
                                                    marginBottom: 20
                                                }} />
                                            }
                                            mapElement={
                                                <div style={{
                                                    position: 'relative',
                                                    top: 0,
                                                    left: 0,
                                                    right: 0,
                                                    bottom: 0,
                                                    width: { adjustedwidth },
                                                    height: 600,
                                                    marginTop: 20,
                                                    marginLeft: 0,
                                                    marginRight: 0,
                                                    marginBottom: 20
                                                }} />
                                            }
                                            markers={this.state.markers}
                                            onMapClick={this.handleMapClick}
                                        />
                                    </div>
                                }
                            </Measure>
                        </Col>
                    </Row>

                    {this.state.isJobAccepted === true ?
                        <Row className="show-grid">
                            <span>
                                <RatingDialog
                                    theId="viewJobCompleteBtn"
                                    headerText="Rate your driver/passenger"
                                    okCallBack={this.ratingsDialogOkCallBack}
                                    actionPerformed={this.state.finalActionHasBeenClicked} />

                                {!(this._authService.isDriver() === true) ?

                                    <YesNoDialog
                                        theId="viewJobCancelBtn"
                                        launchButtonText="Cancel"
                                        actionPerformed={this.state.finalActionHasBeenClicked} 
                                        yesCallBack={this.jobCancelledCallBack}
                                        noCallBack={this.jobNotCancelledCallBack}
                                        headerText="Cancel the job" />
                                    : 
                                    null
                                }

                                <OkDialog
                                    open={this.state.okDialogOpen}
                                    okCallBack={this.okDialogCallBack}
                                    headerText={this.state.okDialogHeaderText}
                                    bodyText={this.state.okDialogBodyText}
                                    key={this.state.okDialogKey} />
                            </span>
                        </Row> :
                        null
                    }
                </Grid>
            </Well>
        );
    }

    handleMapClick = (event) => {

        let currentUser = this._authService.user();
        let isDriver = this._authService.isDriver();
        let matchedMarker = _.find(this.state.markers, { 'email': currentUser.email });
        let newPosition = new Position(event.latLng.lat(), event.latLng.lng());
        let currentJob = this._jobService.currentJob();
        this._positionService.clearUserPosition();
        this._positionService.storeUserPosition(newPosition);

        if (matchedMarker != undefined) {
            let newMarkersList = this.state.markers;
            _.remove(newMarkersList, function (n) {
                return n.email === matchedMarker.email;
            });
            matchedMarker.position = newPosition;
            newMarkersList.push(matchedMarker);
            const newState = Object.assign({}, this.state, {
                currentPosition: newPosition,
                markers: newMarkersList
            })
            this.setState(newState);
            currentJob = matchedMarker.jobForMarker;
        }
        else {
            if (isDriver) {
                let newDriverMarker =
                    this.createDriverMarker(currentUser, event);
                let newMarkersList = this.state.markers;
                newMarkersList.push(newDriverMarker);
                const newState = Object.assign({}, this.state, {
                    currentPosition: newPosition,
                    markers: newMarkersList
                })
                this.setState(newState);
            }
        }
        this._positionService.clearUserJobPositions();
        this._positionService.storeUserJobPositions(this.state.markers);
        this.pushOutJob(newPosition, currentJob);
    }

    handleMarkerClick = (targetMarker) => {

        console.log('button on AcceptList clicked:' + targetMarker.key);
        console.log(targetMarker);

        let currentJob = this._jobService.currentJob();
        let jobForMarker = targetMarker.jobForMarker;

        let clientMarker = _.find(this.state.markers, { 'isDriverIcon': false });
        if (clientMarker != undefined && clientMarker != null) {

            let clientJob = clientMarker.jobForMarker;
            clientJob.driverFullName = jobForMarker.driverFullName;
            clientJob.driverEmail = jobForMarker.driverEmail;
            clientJob.driverPosition = jobForMarker.driverPosition;
            clientJob.vehicleDescription = jobForMarker.vehicleDescription;
            clientJob.vehicleRegistrationNumber = jobForMarker.vehicleRegistrationNumber;
            clientJob.isAssigned = true;
            
            let self = this;
            console.log("handleMarkerClick job");
            console.log(clientJob);

            this.makePOSTRequest('job/submit', clientJob, this,
                function (jdata, textStatus, jqXHR) {
                    console.log("After is accepted");
                    const newState = Object.assign({}, self.state, {
                        isJobAccepted: true
                    })
                    self.setState(newState);
                });
        }
    }

    addMarkerForJob = (jobArgs: any): void => {

        console.log("addMarkerForJob");
        console.log(this.state);

        if (this.state.isJobAccepted || jobArgs.isAssigned) {
            this.processAcceptedMarkers(jobArgs);
        }
        else {
            this.processNotAcceptedMarkers(jobArgs);
        }
    }

    processAcceptedMarkers = (jobArgs: any): void => {

        if (jobArgs.jobUUID != undefined && jobArgs.jobUUID != '')
            this._currentJobUUID = jobArgs.jobUUID;

        let isDriver = this._authService.isDriver();
        let jobClientEmail = jobArgs.clientEmail;
        let jobDriverEmail = jobArgs.driverEmail;
        let newMarkersList = this.state.markers;
        let newPositionForUser = null;
        let newPositionForDriver = null;

        console.log("JOB ACCEPTED WE NEED TO ONLY SHOW THE RELEVANT MARKERS + CURRENT USER");
        //1. Should set all the jobs in markers to assigned now
        //2. Should only show the pair that are in the job if current user is one of them 
        //   otherwise just current user
        let allowedNamed = [this._authService.userEmail()];
        if (this._authService.userEmail() == jobArgs.clientEmail ||
            this._authService.userEmail() == jobArgs.driverEmail) {
            allowedNamed = [jobArgs.clientEmail, jobArgs.driverEmail];
        }
        let finalList: Array<PositionMarker> = new Array<PositionMarker>();
        for (var i = 0; i < this.state.markers.length; i++) {
            if (allowedNamed.indexOf(this.state.markers[i].email) >= 0) {
                let theMarker = this.state.markers[i];
                theMarker.jobForMarker.isAssigned = true;
                finalList.push(theMarker);
            }
        }
        newMarkersList = finalList;

        if (this._authService.userEmail() == jobArgs.clientEmail ||
            this._authService.userEmail() == jobArgs.driverEmail) {

            let clientMarker = _.find(newMarkersList, { 'email': jobArgs.clientEmail });
            if (clientMarker != undefined && clientMarker != null) {
                newPositionForUser = jobArgs.clientPosition;
                clientMarker.position = jobArgs.clientPosition;
            }

            let driverMarker = _.find(newMarkersList, { 'email': jobArgs.driverEmail });
            if (driverMarker != undefined && driverMarker != null) {
                newPositionForUser = jobArgs.driverPosition;
                driverMarker.position = jobArgs.driverPosition;
            }
        }
        else {
            let matchedMarker = _.find(newMarkersList, { 'email': this._authService.userEmail() });
            newPositionForUser = matchedMarker.position;
        }

        //update the state
        this.addClientDetailsToDrivers(newMarkersList);
        var newState = this.updateStateForAcceptedMarker(newMarkersList, newPositionForUser);
        this.updateStateForMarkers(newState, newMarkersList, newPositionForUser, jobArgs);
    }


    processNotAcceptedMarkers = (jobArgs: any): void => {

        if (jobArgs.jobUUID != undefined && jobArgs.jobUUID != '')
            this._currentJobUUID = jobArgs.jobUUID;

        let isDriver = this._authService.isDriver();
        let jobClientEmail = jobArgs.clientEmail;
        let jobDriverEmail = jobArgs.driverEmail;
        let newMarkersList = this.state.markers;
        let newPositionForUser = null;
        let newPositionForDriver = null;

        console.log("JOB NOT ACCEPTED WE NEED TO ONLY ALL");


        //see if the client is in the list (which it may not be). If its not add it, otherwise update it
        if (jobArgs.clientPosition != undefined && jobArgs.clientPosition != null) {
            newPositionForUser = new Position(jobArgs.clientPosition.latitude, jobArgs.clientPosition.longitude);
        }

        if (jobClientEmail != undefined && jobClientEmail != null &&
            newPositionForUser != undefined && newPositionForUser != null) {
            let matchedMarker = _.find(this.state.markers, { 'email': jobClientEmail });
            if (matchedMarker == null) {
                newMarkersList.push(new PositionMarker(
                    jobArgs.clientFullName,
                    newPositionForUser,
                    jobArgs.clientFullName,
                    jobArgs.clientEmail,
                    false,
                    isDriver,
                    jobArgs)
                );
            }
            else {
                if (jobArgs.clientPosition != undefined && jobArgs.clientPosition != null) {
                    this.updateMatchedUserMarker(
                        jobClientEmail,
                        newMarkersList,
                        newPositionForUser,
                        jobArgs);
                }
            }
        }

        //see if the driver is in the list (which it may not be). If its not add it, otherwise update it
        if (jobArgs.driverPosition != undefined && jobArgs.driverPosition != null) {
            newPositionForDriver = new Position(jobArgs.driverPosition.latitude, jobArgs.driverPosition.longitude);
        }

        if (jobDriverEmail != undefined && jobDriverEmail != null &&
            newPositionForDriver != undefined && newPositionForDriver != null) {
            let matchedMarker = _.find(this.state.markers, { 'email': jobDriverEmail });
            if (matchedMarker == null) {
                newMarkersList.push(new PositionMarker(
                    jobArgs.driverFullName,
                    newPositionForDriver,
                    jobArgs.driverFullName,
                    jobArgs.driverEmail,
                    true,
                    isDriver,
                    jobArgs));
            }
            else {
                this.updateMatchedUserMarker(
                    jobDriverEmail,
                    newMarkersList,
                    newPositionForDriver,
                    jobArgs);
            }
        }

        if (isDriver) {
            newPositionForUser = newPositionForDriver;
        }

        //update the state
        this.addClientDetailsToDrivers(newMarkersList);
        var newState = this.updateStateForNewMarker(newMarkersList, newPositionForUser);
        this.updateStateForMarkers(newState, newMarkersList, newPositionForUser, jobArgs);
    }

    addClientDetailsToDrivers = (newMarkersList: PositionMarker[]): void => {
        let clientMarker = _.find(newMarkersList, { 'isDriverIcon': false });
        if (clientMarker != undefined && clientMarker != null) {
            let driverMarkers = _.filter(newMarkersList, { 'isDriverIcon': true });
            for (var i = 0; i < driverMarkers.length; i++) {
                let driversJob = driverMarkers[i].jobForMarker;
                driversJob.jobUUID = clientMarker.jobForMarker.jobUUID;
                driversJob.clientFullName = clientMarker.jobForMarker.clientFullName;
                driversJob.clientEmail = clientMarker.jobForMarker.clientEmail;
                driversJob.clientPosition = clientMarker.jobForMarker.clientPosition;
            }
        }
    }


    updateStateForMarkers = (newState: any, newMarkersList: PositionMarker[], newPositionForUser: Position, jobArgs:any): void => {

        //Update the list of position markers in the PositionService
        this._positionService.clearUserJobPositions();
        this._positionService.storeUserJobPositions(newMarkersList);

        //Update the position in the PositionService
        if (newPositionForUser != undefined && newPositionForUser != null) {
            this._positionService.clearUserPosition();
            this._positionService.storeUserPosition(newPositionForUser);
        }

        this._jobService.clearUserIssuedJob();
        this._jobService.storeUserIssuedJob(jobArgs);

        //update the state
        this.setState(newState);
    }

    updateMatchedUserMarker = (jobEmailToCheck: string, newMarkersList: PositionMarker[],
        jobPosition: Position, jobForMarker:any): void => {

        if (jobEmailToCheck != undefined && jobEmailToCheck != null) {

            let matchedMarker = _.find(this.state.markers, { 'email': jobEmailToCheck });
            if (matchedMarker != null) {
                //update its position
                matchedMarker.position = jobPosition;
                matchedMarker.jobForMarker = jobForMarker;
            }
        }
    }


    updateStateForNewMarker = (newMarkersList:PositionMarker[], position: Position): any => {

        if (position != null) {
            return Object.assign({}, this.state, {
                currentPosition: position,
                markers: newMarkersList
            })
        }
        else {
           return Object.assign({}, this.state, {
                markers: newMarkersList
            })
        }
    }

    updateStateForAcceptedMarker = (newMarkersList: PositionMarker[], position: Position): any => {

        if (position != null) {
            return Object.assign({}, this.state, {
                currentPosition: position,
                markers: newMarkersList,
                isJobAccepted: true
            })
        }
        else {
            return Object.assign({}, this.state, {
                markers: newMarkersList,
                isJobAccepted: true
            })
        }
    }


    shouldShowMarkerForJob = (jobArgs: any): boolean => {

        let isDriver = this._authService.isDriver();
        let currentJob = this._jobService.currentJob();
        let hasJob = currentJob != undefined && currentJob != null;

        //case 1 - No job exists, to allow driver to add their mark initially
        if (!hasJob && isDriver)
            return true;
        
        //case 2 - Job exists and is unassigned and if there is no other active 
        //         job for this client/ driver
        if (hasJob && !currentJob.isAssigned)
            return true;

        //case 3 - If the job isAssigned and its for the current logged in client/driver
        if (hasJob && currentJob.isAssigned) {
            if (currentJob.clientEmail == jobArgs.clientEmail) {
                return true;
            }
            if (currentJob.driverEmail == jobArgs.driverEmail) {
                return true;
            }
        }
        return false;
    }


    pushOutJob = (newPosition: Position, jobForMarker : any): void => {
        var self = this;
        let currentUser = this._authService.user();
        let isDriver = this._authService.isDriver();
        let hasIssuedJob = this._jobService.hasIssuedJob();
        let currentJob = jobForMarker;
        let currentPosition = this._positionService.currentPosition();
        var localClientFullName = '';
        var localClientEmail = '';
        var localClientPosition = null;
        var localDriverFullName = '';
        var localDriverEmail = '';
        var localDriverPosition = null;
        var localIsAssigned = false;

        if (hasIssuedJob) {
            if (currentJob.isAssigned != undefined && currentJob.isAssigned != null) {
                localIsAssigned = currentJob.isAssigned;
            }
            else {
                localIsAssigned = false;
            }
        }

        //clientFullName
        if (hasIssuedJob) {
            if (currentJob.clientFullName != undefined && currentJob.clientFullName != "") {
                localClientFullName = currentJob.clientFullName;
            }
            else {
                localClientFullName = !isDriver ? currentUser.fullName : '';
            }
        }
        //clientEmail
        if (hasIssuedJob) {
            if (currentJob.clientEmail != undefined && currentJob.clientEmail != "") {
                localClientEmail = currentJob.clientEmail;
            }
            else {
                localClientEmail = !isDriver ? currentUser.email : '';
            }
        }
        //clientPosition
        if (hasIssuedJob) {
            if (!isDriver) {
                localClientPosition = newPosition
            }
            else {
                if (currentJob.clientPosition != undefined && currentJob.clientPosition != null) {
                    localClientPosition = currentJob.clientPosition;
                }
            }
        }

        if (hasIssuedJob) {
            //driverFullName
            if (currentJob.driverFullName != undefined && currentJob.driverFullName != "") {
                localDriverFullName = currentJob.driverFullName;
            }
            else {
                localDriverFullName = isDriver ? currentUser.fullName : '';
            }
            //driverEmail
            if (currentJob.driverEmail != undefined && currentJob.driverEmail != "") {
                localDriverEmail = currentJob.driverEmail;
            }
            else {
                localDriverEmail = isDriver ? currentUser.email : '';
            }

            //driverPosition
            if (isDriver) {
                localDriverPosition = newPosition
            }
            else {
                if(currentJob.driverPosition != undefined && currentJob.driverPosition != null) {
                    localDriverPosition = currentJob.driverPosition;
                }
            }
        }
        else {
            localDriverFullName = currentUser.fullName;
            localDriverEmail = currentUser.email;
            localDriverPosition = isDriver ? currentPosition : null;
        }

        var newJob = {
            jobUUID: this._currentJobUUID != undefined && this._currentJobUUID != '' ?
                this._currentJobUUID : '',
            clientFullName: localClientFullName,
            clientEmail: localClientEmail,
            clientPosition: localClientPosition,
            driverFullName: localDriverFullName,
            driverEmail: localDriverEmail,
            driverPosition: localDriverPosition,
            vehicleDescription: isDriver ?
                this._authService.user().vehicleDescription : '',
            vehicleRegistrationNumber: isDriver ?
                this._authService.user().vehicleRegistrationNumber : '',
            isAssigned: localIsAssigned,
            isCompleted: false
        }

        console.log("handlpushOutJob job");
        console.log(newJob);
        this.makePOSTRequest('job/submit', newJob, self,
            function (jdata, textStatus, jqXHR) {
                self._jobService.clearUserIssuedJob();
                self._jobService.storeUserIssuedJob(newJob);
            });
    }

    createDriverMarker = (
        driver: any,
        event: any): PositionMarker => {

        let localDriverFullName = driver.fullName;
        let localDriverEmail = driver.email;
        let localDriverPosition = new Position(event.latLng.lat(), event.latLng.lng());
        let localVehicleDescription = this._authService.user().vehicleDescription;
        let localVehicleRegistrationNumber = this._authService.user().vehicleRegistrationNumber;
        let currentUserIsDriver = this._authService.isDriver();

        var driverJob = {
            jobUUID: this._currentJobUUID != undefined && this._currentJobUUID != '' ?
                this._currentJobUUID : '',

            driverFullName: localDriverFullName,
            driverEmail: localDriverEmail,
            driverPosition: localDriverPosition,
            vehicleDescription: localVehicleDescription,
            vehicleRegistrationNumber: localVehicleRegistrationNumber,
            isAssigned: false,
            isCompleted: false
        }		

        return new PositionMarker(
            localDriverFullName,
            localDriverPosition,
            localDriverFullName,
            localDriverEmail,
            true,
            currentUserIsDriver,
            driverJob
        );
    }


    
    ratingsDialogOkCallBack = (theRatingScore: number) => {
        console.log('RATINGS OK CLICKED');

        var self = this;
        let currentUser = this._authService.user();
        let isDriver = this._authService.isDriver();
        let currentJob = this._jobService.currentJob();
        var ratingJSON = null;

        if (!isDriver) {
            ratingJSON = {
                fromEmail: this._authService.userEmail(),
                toEmail: currentJob.driverEmail,
                score: theRatingScore
            }
        }
        else {
            ratingJSON = {
                fromEmail: this._authService.userEmail(),
                toEmail: currentJob.clientEmail,
                score: theRatingScore
            }
        }

        this.makePOSTRequest('rating/submit/new', ratingJSON, self,
            function (jdata, textStatus, jqXHR) {
                this._jobService.clearUserIssuedJob();
                this._positionService.clearUserJobPositions();
                this.setState(
                    {
                        okDialogHeaderText: 'Ratings',
                        okDialogBodyText: 'Rating successfully recorded',
                        okDialogOpen: true,
                        okDialogKey: Math.random(),
                        markers: new Array<PositionMarker>(),
                        currentPosition: null,
                        isJobAccepted: false,
                        finalActionHasBeenClicked: true
                    });
            });
    }

   
    makePOSTRequest = (route: string, jsonData: any, context: ViewJob, doneCallback: DoneCallback) => {
        $.ajax({
            type: 'POST',
            url: route,
            data: JSON.stringify(jsonData),
            contentType: "application/json; charset=utf-8",
            dataType: 'json'
        })
        .done(function (jdata, textStatus, jqXHR) {
            doneCallback(jdata, textStatus, jqXHR);
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            const newState = Object.assign({}, context.state, {
                okDialogHeaderText: 'Error',
                okDialogBodyText: jqXHR.responseText,
                okDialogOpen: true,
                okDialogKey: Math.random()
            })
            context.setState(newState)
        });
    }

    jobCancelledCallBack = () => {
        console.log('CANCEL YES CLICKED');
        this._jobService.clearUserIssuedJob();
        this._positionService.clearUserJobPositions();
        this.setState(
            {
                okDialogHeaderText: 'Job Cancellaton',
                okDialogBodyText: 'Job successfully cancelled',
                okDialogOpen: true,
                okDialogKey: Math.random(),
                markers: new Array<PositionMarker>(),
                currentPosition: null,
                isJobAccepted: false,
                finalActionHasBeenClicked: true
            });
    }

    jobNotCancelledCallBack = () => {
        console.log('CANCEL NO CLICKED');
        this.setState(
            {
                okDialogHeaderText: 'Job Cancellaton',
                okDialogBodyText: 'Job remains open',
                okDialogOpen: true,
                okDialogKey: Math.random(),
                finalActionHasBeenClicked: true
            });
    }

    okDialogCallBack = () => {
        console.log('OK on OkDialog CLICKED');
        this.setState(
            {
                okDialogOpen: false
            });
    }
}

 

 

Some highlights

  • We use RX.Js to listen to new events straight over the Comet based forever frame, that the server side Play scala code pushes a message out on
  • There was a funny thing with driver acceptance which I originally wanted to be a button on a drivers marker within the map. However this caused an issue with the Map where it would get a Map event when clicking on an overlay (higher Z-Order so should not happen). This is a feature of the React Google Map component. I could not find a fix I liked (I did mess around with form event mouseEnter/mouseLeave but it was just not that great, so I opted to chose to put the acceptance of driver outside of the map, thus avoiding the issue altogether)

 

If you curious how to run and see what you need to install, please see the README.MD in the codebase that has full instructions

 

 

What’s it looks like when its run?

Some scenarios of what it looks like running are shown below.

In order to run it to this point,I normally follow this set of steps afterwards

 

  • open a tab, login as a passenger that I had created
  • go to the “create job” page, click the map, push the “create job” button
  • open a NEW tab, login as a new driver, go to the “view job” page
  • on the 1st tab (passenger) click the map to push passenger position to driver
  • on the 2nd tab (driver) click the map to push driver position to passenger
  • repeat last 4 steps for additional driver
  • on client tab pick driver to accept, click accept button
  • complete the job from client tab, give driver rating
  • complete the job from paired driver tab, give passenger rating
  • go to “view rating” page, should see ratings

 

One of the challenges with an app like this, is that it is a streaming app. So this means that when a client pushes out a new job, there may be no-one listening for that job. Drivers may not even be logged in at all, or may login later, so they effectively subscribe late. So for this app dealing with that was kind of out of scope. So to remedy this, you need to ensure that position updates (clicking on the map for the given user browser session (ie tab)) gets pushed to other user browser sessions where the marker is not currently shown.

 

In a real world app, we might choose to do one of the following to fix this permanently

  • Store some, and when a new user joins grab all unassigned passengers/driver within some geographical area
  • Store last N-Many passenger/driver positions and push these on new login (there is no guarantee that these are in the same geographical  area as us though, could be completely unrelated/of no practical concern for the current user)

 

Anyway as I say this out of scope for this demo project, but I hope that it does give you some insight as to why you need to push position updates manually

 

This gives you an example of what it all looks like when its running (not accepted yet)

This is what it looks like for the following setup

 

1 x passenger (sacha barber)

2 x driver (driver 1 / driver 2)

 

Passenger (sacha) sees this

 

image

 

Driver 1 sees this

image

 

Driver 2 sees this

image

 

So now lets see what happens when we accept one of the drivers. I have chosen “driver 1” for this example

 

 

 

This gives you an example of what it all looks like when its running (after job accepted between passenger/driver1)

Here is what things look like after job has been accepted

 

Passenger (sacha) sees this

See how now only the passenger (sacha) and the driver chosen (driver 1) are now shown

image

 

Driver 1 sees this

See how now only the passenger (sacha) and the driver chosen (driver 1) are now shown

image

 

Driver 2 sees this

Since this driver (driver 2) was not chosen this drivers session now only shows itself

image

 

 

 

 

 

 

Conclusion / Errors Made Along The Way

As with any reasonable size project (and this definitely is in that category for me) mistakes will be made. And whilst I am happy that I got all of this to work, I have certainly made a few mistakes, such as

 

  • There should have been 2 streams of jobs, client and driver
  • There should have been no separation between creating a job and viewing a job
  • There was a funny thing with driver acceptance which I originally wanted to be a button on a drivers marker within the map. However this caused an issue with the Map where it would get a Map event when clicking on an overlay (higher Z-Order so should not happen). This is a feature of the React Google Map component. I could not find a fix I liked (I did mess around with form event mouseEnter/mouseLeave but it was just not that great, so I opted to chose to put the acceptance of driver outside of the map, thus avoiding the issue altogether)
  • There is currently a bug when a job becomes paired between a passenger/driver, and new position updates are not reflected. This is probably a couple of line change, but at this stage I’m like MEH. I am kind of done, I proved what I want to try, and the main points I set out to prove all work without this, so Math.Power(MEH, Infinity)
  • When a driver joins (possibly later after other drivers), they should instantly know about other drivers/client. Right now the users who’s position you are missing would need to push a position update to get that user reflected on the users screen who is missing that user. I could have done something where every X milliseconds ALL current user positions are pushed to all other users, but this would clearly not scale. Another alternative would have been to store the state of all uses positions in a database every time that the map was clicked, and then when a new user joins find all users/jobs in the general area providing they are not already assigned to a job. Being completely honest this was not really what this project was about either. The main cut and thrust of this article was to mess around with Kafka Streams and see how they work, and how I could stream stuff on top of that all the way back to a browser session. To this end the project has been a massive success

 

On a personal note, I have had a great time writing this project and have learnt loads doing it. For example I am now way more familiar with how Akka Streams work, and I now know Play and Webpack to a fairly good level. I thoroughly recommend ALL of you pick your own mad cap projects and roll with them.

 

For me next I am either going to be dipping back into Azure stuff, or more scala where we will learn cats and Shapeless.