KatNav - A Kubernetes Sat Nav

Create the project with kubebuilder

1
2
3
kubebuilder init --domain fnnrn.me --owner "Dan Finneran"

kubebuilder create api --group katnav --kind Directions --version v1

Edit the Resources

The path is api/v1/directions_types.go

Edit the Directions Specification DirectionsSpec

The example is the usual Foo etc..

1
2
// Foo is an example field of Directions. Edit directions_types.go to remove/update
Foo string `json:"foo,omitempty"`

We will change this to what we need for the directions to actually work:

1
2
3
4
5
// Source is where the beginning of our journey is
Source string `json:"source"`

// Destination is the end of our journey
Destination string `json:"destination"`

Edit the Directions Status DirectionsStatus

The Status reflects what has happened to our resource, and the expected status will be the directions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Directions is a list of directions to our destination
Directions string `json:"directions"`

// Routesummary gives a simple overview of the route
RouteSummary string `json:"routeSummary"`

// StartLocation is the start from the directions API
StartLocation string `json:"startLocation"`

// EndLocation is the start from the directions API
EndLocation string `json:"endLocation"`

// Distance is the total distance of the journey
Distance string `json:"distance"`

// Duration is the amount of time the journey will take
Duration string `json:"duration"`

// Error captures an error message if the route isn't possible
Error string `json:"error,omitempty"`

Configure Google Account

To get an API key:

  1. Visit Google APIs Console and log in with a Google Account.
  2. Select one of your existing projects, or create a new project.
  3. Enable the API(s) you want to use. The Go Client for Google Maps Services accesses the following APIs:
  • Directions API

Create a new Server key from the credentials menu.

Billing may need to be enabled so do be careful with usage!

Apply API key to secret

This will create our secret (called katnav) within the β€œdefault” namespace:

1
kubectl create secret generic katnav --from-literal=directionsKey=<API TOKEN>

Add our code to the controller

The code that we’re going to add to the controller will do a few things:

  1. Connect to the Google Maps API
  2. Watch for directions resources being created within the Kubernetes API
  3. Update these resources (the status) with the directions and additional journey information

Add the Google Maps client

Add "googlemaps.github.io/maps" to the imports and a pointer to the client in the DirectionsReconciler struct:

1
2
3
4
5
6
// DirectionsReconciler reconciles a Directions object
type DirectionsReconciler struct {
client.Client
Scheme *runtime.Scheme
mClient *maps.Client
}

Initialise the Google Maps client

At the beginning of the SetupWithManager() function we will add in code that will do the following:

  1. Create our own Kubernetes client
  2. Retrieve our secret in the default namespace
  3. Create our maps client and update out pointer to the client so we can use it in the reconcile() function later:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// So we have a Kubernetes cluster in r.Client, however we can't use it until the caches
// start otherwise it will just return an error. So in order to get things ready we will
// use our own client in order to get the key and set up the Google Maps client in advance
config, err := restClient.InClusterConfig()
if err != nil {
kubeConfig :=
cmdClient.NewDefaultClientConfigLoadingRules().GetDefaultFilename()
config, err = cmdClient.BuildConfigFromFlags("", kubeConfig)
if err != nil {
return err
}
}
// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}

secret, err := clientset.CoreV1().Secrets("default").Get(context.TODO(), "katnav", v1.GetOptions{})
if err != nil {
return err
}

token := string(secret.Data["directionsKey"])
if token == "" {
return fmt.Errorf("no Token found within API Key")
}
r.mClient, err = maps.NewClient(maps.WithAPIKey(token))
if err != nil {
return err
}

Reconcile !

Below is our full Reconcile() function that does the following:

  1. Retrieves our resource that needs reconciling
  2. Gets the source/destination from the spec
  3. Creates a Google Maps Directions request to the API
  4. Updates the status of the resource with the direction information (that we format, such as removing the HTML tags etc..)
  5. Apply the updated directions Object to the Kubernetes API!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
func (r *DirectionsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Set the log to an acutal value so we can create log messages
log := log.FromContext(ctx)
log.Info("Reconciling direction resources")

var directions katnavv1.Directions
if err := r.Get(ctx, req.NamespacedName, &directions); err != nil {
if errors.IsNotFound(err) {
// object not found, could have been deleted after
// reconcile request, hence don't requeue
return ctrl.Result{}, nil
}
log.Error(err, "unable to fetch Directions object")
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// your logic here
log.Info("Determining journey", "Source", directions.Spec.Source, "Destination", directions.Spec.Destination)

request := &maps.DirectionsRequest{
Origin: directions.Spec.Source,
Destination: directions.Spec.Destination,
Mode: maps.TravelModeDriving,
}

route, _, err := r.mClient.Directions(context.Background(), request)
if err != nil {
log.Error(err, "unable to fetch Directions")
}
var directionsString string
if len(route) == 0 {
return ctrl.Result{}, nil
}

log.Info("New Route", "Summary", route[0].Summary)
for x := range route[0].Legs {
for y := range route[0].Legs[x].Steps {
stripped := strip.StripTags(route[0].Legs[x].Steps[y].HTMLInstructions)
directionsString += stripped + "\n"
}
directions.Status.Distance = route[0].Legs[x].Distance.HumanReadable
directions.Status.Duration = fmt.Sprintf("Total Minutes: %f", route[0].Legs[x].Duration.Minutes())
directions.Status.StartLocation = route[0].Legs[x].StartAddress
directions.Status.EndLocation = route[0].Legs[x].EndAddress

}

directions.Status.RouteSummary = route[0].Summary
directions.Status.Directions = directionsString

err = r.Client.Status().Update(context.TODO(), &directions, &client.UpdateOptions{})
if err != nil {
log.Error(err, "unable to update journey")
}
return ctrl.Result{}, nil
}

Using !

Sheffield.yaml

This is our manifest for directions from York -> Sheffield!

1
2
3
4
5
6
7
apiVersion: katnav.fnnrn.me/v1
kind: Directions
metadata:
name: directions-to-sheffield
spec:
source: York, uk
destination: Sheffield, uk

We can apply this to Kubernetes with:

1
kubectl apply -f ./sheffield.yaml

We should see the following logs from the Controller:

1
2
3
2021-07-28T15:56:47.901+0100    INFO    controller-runtime.manager.controller.directions        New Route       {"reconciler group": "katnav.fnnrn.me", "reconciler kind": "Directions", "name": "directions-to-sheffield", "namespace": "default", "Summary": "M1"}
2021-07-28T15:56:47.906+0100 INFO controller-runtime.manager.controller.directions Reconciling direction resources {"reconciler group": "katnav.fnnrn.me", "reconciler kind": "Directions", "name": "directions-to-sheffield", "namespace": "default"}
2021-07-28T15:56:47.906+0100 INFO controller-runtime.manager.controller.directions Determining journey {"reconciler group": "katnav.fnnrn.me", "reconciler kind": "Directions", "name": "directions-to-sheffield", "namespace": "default", "Source": "York, uk", "Destination": "Sheffield, uk"}

Looking at our Directions πŸ‘€

If we examine the newly created directions object we should see that it’s status has been updated accordingly !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ kubectl get directions directions-to-sheffield -o yaml
apiVersion: katnav.fnnrn.me/v1
kind: Directions
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"katnav.fnnrn.me/v1","kind":"Directions","metadata":{"annotations":{},"name":"directions-to-sheffield","namespace":"default"},"spec":{"destination":"Sheffield, uk","source":"York, uk"}}
creationTimestamp: "2021-07-28T14:34:56Z"
generation: 2
name: directions-to-sheffield
namespace: default
resourceVersion: "6986253"
uid: c4077b0b-8f2f-4fc3-82c0-2d690a24d98a
spec:
destination: Sheffield, uk
source: York, uk
status:
directions: |
Head southwest on Lendal Bridge/Station Rd/A1036 toward Rougier St/B1227Continue to follow A1036
Keep left to continue toward Station Rd/A1036
Continue onto Station Rd/A1036Continue to follow A1036
Turn right onto Blossom St/A1036Continue to follow A1036
At the roundabout, take the 2nd exit onto Tadcaster Rd Dringhouses/A1036
Take the ramp to Leeds
Merge onto A64
Merge onto A1(M) via the ramp to Leeds/M1/Manchester/M62
Keep right at the fork to continue on M1
At junction 34, take the A6109 exit to Sheffield(E)/Rotherham(C)/Meadowhall
At Meadowhall Roundabout, take the 4th exit onto Meadowhall Rd/A6109
Keep right to stay on Meadowhall Rd/A6109Continue to follow A6109
At the roundabout, take the 1st exit onto Brightside Ln/A6109Continue to follow A6109
Slight right onto Savile St/A6109
Turn right onto Derek Dooley Way/A61Continue to follow A61
Slight left onto Corporation St/A61
Slight left onto Corporation St/B6539
At the roundabout, take the 2nd exit onto W Bar Green/B6539Continue to follow B6539
At the roundabout, take the 3rd exit onto Broad Ln/B6539Continue to follow B6539
At the roundabout, take the 1st exit onto Upper Hanover St
Continue onto Hanover Way
At the roundabout, take the 1st exit onto Moore St
Continue onto Charter Row
Continue onto Furnival Gate
Furnival Gate turns left and becomes Pinstone St
Turn right onto Burgess St
Burgess St turns right and becomes Barker's Pool
Barker's Pool turns left and becomes Leopold St
distance: 93.8 km
duration: 'Total Minutes: 77.816667'
endLocation: Sheffield, UK
routeSummary: M1
startLocation: York, UK