Digital Developer Conference: Cloud Security 2021 -- Build the skills to secure your cloud and data Register free

Develop and deploy a Level I JanusGraph Operator using BerkeleyDB

This tutorial shows you how to develop and deploy a Level I operator on the Red Hat OpenShift Container Platform. We use the Operator Capability Levels as the guideline for what is considered a Level I operator.

You will learn how to deploy JanusGraph using the default backend storage, BerkeleyDB. This is much simpler to deploy, since it only uses one pod and doesn’t use any persistent volumes to your cluster. This approach is only recommended for testing purposes, as noted in the JanusGraph docs, and makes it much easier to get up and running quickly — so we will start with this approach. After you’ve done that, you can move on to a more advanced approach using Cassandra for the backend storage.

Prerequisites

For this tutorial, we assume you are looking for deep technical knowledge of how to implement a Level I operator. We also assume that you have the following technical experience and know-how:

In addition, you also need to have completed the environment setup.

Estimated time

It should take you about 1 hour to complete this tutorial.

Steps

  1. Overview
  2. Clone and modify the JanusGraph Docker image
  3. Create the JanusGraph project and API using Operator SDK
  4. Update the JanusGraph API
  5. Controller Logic: Creating a Service
  6. Controller Logic: Creating a StatefulSet
  7. Update the user and the CR
  8. Build, push, and deploy your operator
  9. Verify the operator

1. Overview

What is a Level I operator?

As defined by the Operator Capability Levels, a Level I operator is one that performs “automated application provisioning and configuration management.”

Requirements for a Level I operator

Your operator should include the following capabilities to qualify as a Level I operator:

  • It should provision an application via a custom resource (CR).
  • It should enable all installation configuration details to be specified in the spec section of the CR.
  • It should be possible to install the operator in multiple ways (such as kubectl, OLM, or OperatorHub).
  • All configuration files should be able to be created within Kubernetes.
  • It must wait for the operand to reach a healthy state.
  • It must use the status sub-resource of the CR to communicate with the user when the operand has reconciled.

JanusGraph example

Now that you understand at a high level what an operator must do to be considered Level I, let’s dive into our example.

This tutorial uses JanusGraph as the example of the service you want to create an operator for. As of the time of this writing, there is no JanusGraph operator on OperatorHub. JanusGraph is a distributed, scalable, open source graph database.

Note: JanusGraph is an example. You can apply the concepts you learn here to any application or service you want to create an operator for.

JanusGraph operator with BerkeleyDB requirements

Next, it’s important that you understand what the JanusGraph operator must to do to successfully run JanusGraph on OpenShift. More specifically, you will learn how to implement the following changes in the controller code that runs each time a change to the CR is observed:

  1. Create a Service if one does not exist.
  2. Create a StatefulSet if ones does not exist.
  3. Update the status of the JanusGraph service that’s running in the StatefulSet.

To get the default JanusGraph configuration (using BerkeleyDB) up and running, your operator only needs to create these two resources, the service and the StatefulSet. As explained in StatefulSet limitations, the StatefulSet needs to have a headless service that is responsible for the network identity of the pods.

What is a StatefulSet?

A StatefulSet is the Kubernetes object that is used to manage stateful applications. Similar to a Deployment, a StatefulSet manages pods that are based on an identical container spec. The difference is that in a Deployment the pods are interchangeable, whereas in a StatefulSet they are not — each has a unique identifier that is maintained across any rescheduling.

2. Clone and modify the JanusGraph Docker image

The JanusGraph Docker image from the official repo deploys fine into Kubernetes, but runs into errors when deployed into OpenShift. There are few things that need to be modified before you can deploy.

Follow the instructions to create a JanusGraph image that can be deployable to OpenShift.

Note: Make sure to use this modified image in your controller logic in future steps.

3. Create the JanusGraph project and API using Operator SDK

The tutorial “Build and deploy a basic operator” explains how to use the Operator SDK to scaffold an operator. To do that for the JanusGraph operator, let’s start by creating the JanusGraph project and the API.

First, create your project directory:

mkdir $HOME/projects/janusgraph-operator
cd $HOME/projects/janusgraph-operator

Then create your project:

operator-sdk init --domain=example.com --repo=github.com/example/janusgraph-operator

For Go modules to work properly, you need to activate GO module support by running the following command:

export GO111MODULE=on

Next, create the API, with the kind being Janusgraph:

operator-sdk create api --group=graph --version=v1alpha1 --kind=Janusgraph --controller --resource

You should see output like this:

Writing scaffold for you to edit...
api/v1alpha1/janusgraph_types.go
controllers/janusgraph_controller.go
Running make:
...

4. Update the JanusGraph API

Next, let’s update the API. Your janusgraph_types.go file should look like the following:

package v1alpha1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// JanusgraphSpec defines the desired state of Janusgraph
type JanusgraphSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Foo is an example field of Janusgraph. Edit Janusgraph_types.go to remove/update
    Size    int32  `json:"size"`
    Version string `json:"version"`
}

// JanusgraphStatus defines the observed state of Janusgraph
type JanusgraphStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file
    Nodes []string `json:"nodes"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Janusgraph is the Schema for the janusgraphs API
type Janusgraph struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   JanusgraphSpec   `json:"spec,omitempty"`
    Status JanusgraphStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// JanusgraphList contains a list of Janusgraph
type JanusgraphList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []Janusgraph `json:"items"`
}

func init() {
    SchemeBuilder.Register(&Janusgraph{}, &JanusgraphList{})
}

As shown above, you need to add the Size and Version fields to the Spec, and add the Spec and Status fields to the Janusgraph struct. (This should be familiar to you if you’ve completed “Build and deploy a basic operator” which offers more details about using the Operator SDK.)

5. Controller Logic: Creating a Service

Note: If you want to learn more in-depth about the controller logic than is written here, read “Explanation of Memcached operator code.”

Now that you have your API updated, the next step is to implement your controller logic in controllers/janusgraph_controller.go. First, go ahead and copy the code from the artifacts/janusgraph_controller.go file, and replace your current controller code.

Once this is complete, your controller should look like the following:

/*
Copyright 2021.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
    "context"
    "reflect"

    "github.com/cloudflare/cfssl/log"
    "github.com/go-logr/logr"
    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/types"
    "k8s.io/apimachinery/pkg/util/intstr"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"

    "github.com/example/janusgraph-operator/api/v1alpha1"
    graphv1alpha1 "github.com/example/janusgraph-operator/api/v1alpha1"
)

// JanusgraphReconciler reconciles a Janusgraph object
type JanusgraphReconciler struct {
    client.Client
    Log    logr.Logger
    Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=graph.example.com,resources=janusgraphs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=graph.example.com,resources=janusgraphs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=graph.example.com,resources=janusgraphs/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=pods;deployments;statefulsets;services;persistentvolumeclaims;persistentvolumes;,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods;services;persistentvolumeclaims;persistentvolumes;,verbs=get;list;create;update;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Janusgraph object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
func (r *JanusgraphReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("janusgraph", req.NamespacedName)

    // Fetch the Janusgraph instance
    janusgraph := &graphv1alpha1.Janusgraph{}
    err := r.Get(ctx, req.NamespacedName, janusgraph)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
            // Return and don't requeue
            log.Info("Janusgraph resource not found. Ignoring since object must be deleted")
            return ctrl.Result{}, nil
        }
        // Error reading the object - requeue the request.
        log.Error(err, "Failed to get Janusgraph")
        return ctrl.Result{}, err
    }

    var result *ctrl.Result
    service := r.serviceForJanusgraph(janusgraph)

    //ensureService returns nil once a service with name janusgraph is found in the given namespace
    result, err = r.ensureService(ctx, janusgraph, service)

    if result != nil {
        return *result, err
    }

    statefulSetDep := r.statefulSetForJanusgraph(janusgraph)

    //ensureStatefulSet returns nil once a statefulset with name janusgraph is found in the given namespace
    result, err = r.ensureStatefulSet(ctx, janusgraph, statefulSetDep)
    if result != nil {
        return *result, err
    }

    found := &appsv1.StatefulSet{}
    err = r.Get(ctx, types.NamespacedName{Name: janusgraph.Name, Namespace: janusgraph.Namespace}, found)
    // Ensure the statefulset's replicas are the same as defined in the spec section of the custom resource
    size := janusgraph.Spec.Size
    if *found.Spec.Replicas != size {
        found.Spec.Replicas = &size
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }

    // look for resource of type PodList
    podList := &corev1.PodList{}
    //create filter to check for Pods only in our Namespace with the correct matching labels
    listOpts := []client.ListOption{
        client.InNamespace(janusgraph.Namespace),
        client.MatchingLabels(labelsForJanusgraph(janusgraph.Name)),
    }
    //List all Pods that match our filter (same Namespace and matching labels)
    if err = r.List(ctx, podList, listOpts...); err != nil {
        log.Error(err, "Failed to list pods", "Janusgraph.Namespace", janusgraph.Namespace, "Janusgraph.Name", janusgraph.Name)
        return ctrl.Result{}, err
    }
    //return an array of pod names
    podNames := getPodNames(podList.Items)

    // Update the status of our JanusGraph object to show Pods which were returned from getPodNames
    if !reflect.DeepEqual(podNames, janusgraph.Status.Nodes) {
        janusgraph.Status.Nodes = podNames
        err := r.Status().Update(ctx, janusgraph)
        if err != nil {
            log.Error(err, "Failed to update Janusgraph status")
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{}, nil
}

// getPodNames returns a string array of Pod Names
func getPodNames(pods []corev1.Pod) []string {
    var podNames []string
    for _, pod := range pods {
        podNames = append(podNames, pod.Name)
    }
    return podNames
}

// SetupWithManager sets up the controller with the Manager.
func (r *JanusgraphReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&graphv1alpha1.Janusgraph{}).
        Complete(r)
}

// labelsForJanusgraph returns a map of string keys and string values
func labelsForJanusgraph(name string) map[string]string {
    return map[string]string{"app": "Janusgraph", "janusgraph_cr": name}
}

// serviceForJanusgraph returns a Load Balancer service for our JanusGraph object
func (r *JanusgraphReconciler) serviceForJanusgraph(m *v1alpha1.Janusgraph) *corev1.Service {


    //fetch labels
    ls := labelsForJanusgraph(m.Name)
    //create Service
    srv := &corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      m.Name + "-service",
            Namespace: m.Namespace,
        },
        Spec: corev1.ServiceSpec{
            Type: corev1.ServiceTypeLoadBalancer,
            Ports: []corev1.ServicePort{
                {
                    Port: 8182,
                    TargetPort: intstr.IntOrString{
                        IntVal: 8182,
                    },
                    NodePort: 30180,
                },
            },
            Selector: ls,
        },
    }
    ctrl.SetControllerReference(m, srv, r.Scheme)
    return srv
}

// statefulSetForJanusgraph returns a StatefulSet for our JanusGraph object
func (r *JanusgraphReconciler) statefulSetForJanusgraph(m *v1alpha1.Janusgraph) *appsv1.StatefulSet {
    log.Info("after statefulSetDep in reconcile ")

    //fetch labels
    ls := labelsForJanusgraph(m.Name)
    //fetch the size of the JanusGraph object from the custom resource
    replicas := m.Spec.Size
    //fetch the version of JanusGraph to install from the custom resource
    version := m.Spec.Version

    //create StatefulSet
    statefulSet := &appsv1.StatefulSet{
        ObjectMeta: metav1.ObjectMeta{
            Name:      m.Name,
            Namespace: m.Namespace,
        },
        Spec: appsv1.StatefulSetSpec{
            Replicas: &replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: ls,
            },
            ServiceName: m.Name + "-service",
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: ls,
                    Name:   "janusgraph",
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Image: "horeaporutiu/janusgraph:" + version,
                            Name:  "janusgraph",
                            Ports: []corev1.ContainerPort{
                                {
                                    ContainerPort: 8182,
                                    Name:          "janusgraph",
                                },
                            },
                            Env: []corev1.EnvVar{},
                        }},
                    RestartPolicy: corev1.RestartPolicyAlways,
                },
            },
        },
    }
    ctrl.SetControllerReference(m, statefulSet, r.Scheme)
    return statefulSet
}

//ensureStatefulSet checks for a resource of type StatefulSet with a given name in a given namespace and creates one if one does not exist
//ensureStatefulSet returns nil, nil if it finds StatefulSet with name janusgraph in the given namespace
func (r *JanusgraphReconciler) ensureStatefulSet(ctx context.Context, janusgraph *graphv1alpha1.Janusgraph, dep *appsv1.StatefulSet,
) (*ctrl.Result, error) {
    // look for a resource of type StatefulSet
    found := &appsv1.StatefulSet{}
    // Check if the StatefulSet already exists in our namespace, if not create a new one
    err := r.Get(ctx, types.NamespacedName{Name: janusgraph.Name, Namespace: janusgraph.Namespace}, found)
    if err != nil && errors.IsNotFound(err) {
        log.Info("Creating a new Statefulset", "StatefulSet.Namespace", dep.Namespace, "StatefulSet.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new StatefulSet", "StatefulSet.Namespace", dep.Namespace, "StatefulSet.Name", dep.Name)
            return &ctrl.Result{}, err
        }
        // StatefulSet created successfully - return and requeue
        log.Info("StatefulSet created, requeuing")
        return &ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get StatefulSet")
        return &ctrl.Result{}, err
    }
    return nil, nil
}

//ensureService checks for a resource of type Service with a given name in a given namespace and creates one if one does not exist
//ensureService returns nil, nil if it finds Service with name janusgraph in the given namespace
func (r *JanusgraphReconciler) ensureService(ctx context.Context, janusgraph *graphv1alpha1.Janusgraph,
    srv *corev1.Service) (*ctrl.Result, error) {
    serviceFound := &corev1.Service{}
    //check for Service resources in our namespace, and with a "JanusGraph" name prefix
    err := r.Get(ctx, types.NamespacedName{Name: janusgraph.Name + "-service", Namespace: janusgraph.Namespace}, serviceFound)
    if err != nil && errors.IsNotFound(err) {

        err = r.Create(ctx, srv)
        if err != nil {
            log.Error(err, "Failed to create new service", "service.Namespace", srv.Namespace, "service.Name", srv.Name)
            return &ctrl.Result{}, err
        }
        // Service created successfully - return and requeue
        return &ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get service")
        return &ctrl.Result{}, err
    }
    return nil, nil
}

Now, let’s take a closer look at the controller code above. The first thing you need to do at a high level to create an operator for JanusGraph is to create a headless service. This is a service for accessing a collection of pods, like any service, but one that has no cluster IP address on the network and thus performs no load balancing.

First, check to see if there is a service:

var result *ctrl.Result
service := r.serviceForJanusgraph(janusgraph)

//ensureService returns nil once a service with name janusgraph is found in the given namespace
result, err = r.ensureService(ctx, janusgraph, service)

if result != nil {
    return *result, err
}

If ensureService returns nil, then you can continue with the rest of the operator logic. Otherwise, if you create a new service (i.e. the service that is passed in as the third argument to ensureService — which you created by calling serviceForJanusgraph()) then you should requeue.

Service for JanusGraph

Let’s look at the serviceForJanusgraph(janusgraph) function in more detail. Here is the function signature:

func (r *JanusgraphReconciler) serviceForJanusgraph(m *v1alpha1.Janusgraph) *corev1.Service

This means that you pass in a JanusGraph object and return a corev1.Service.

Below, you can see the full serviceForJanusgraph function:

// serviceForJanusgraph returns a Load Balancer service for our JanusGraph object
func (r *JanusgraphReconciler) serviceForJanusgraph(m *graphv1alpha1.Janusgraph) *corev1.Service {

    //fetch labels
    ls := labelsForJanusgraph(m.Name)
    //create Service
    srv := &corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      m.Name + "-service",
            Namespace: m.Namespace,
        },
        Spec: corev1.ServiceSpec{
            Type: corev1.ServiceTypeLoadBalancer,
            Ports: []corev1.ServicePort{
                {
                    Port: 8182,
                    TargetPort: intstr.IntOrString{
                        IntVal: 8182,
                    },
                },
            },
            Selector: ls,
        },
    }
    ctrl.SetControllerReference(m, srv, r.Scheme)
    return srv
}

Let’s now compare this implementation to a similar YAML implementation by analyzing the picture below.

operator structure

You can see that the YAML and Golang implementations of a Kubernetes service are very similar, with small syntax differences:

  1. To create a specific Kubernetes resource, you use the apiVersion and kind keywords in YAML, versus corev1.Service in Golang.
  2. The syntax for metadata is slightly different — YAML is metadata while in Golang is ObjectMeta.
  3. You can see that above the port statement, you specify type LoadBalancer. You also set both port and targetPort to be 8182. Note that port 8182 comes from the official JanusGraph Docker image. This means that when you configure your own service, you should read the documentation to determine which port the image should listen on.
  4. The pods need to have a label so that the service can find them using a selector.

6. Controller Logic: Creating a StatefulSet

Next, you need to create a StatefulSet for JanusGraph. You can see that the code is very similar to that used to create a service for JanusGraph, other than some minor details with creating the StatefulSet object itself. Note that instead of a Deployment, you use a StatefulSet — but this same logic can be applied to the deployment object.

StatefulSet for JanusGraph

Let’s dive into the statefulSetForJanusgraph(janusgraph) function. Here’s what it looks like:

// statefulSetForJanusgraph returns a StatefulSet for our JanusGraph object
func (r *JanusgraphReconciler) statefulSetForJanusgraph(m *graphv1alpha1.Janusgraph) *appsv1.StatefulSet {

    //fetch labels
    ls := labelsForJanusgraph(m.Name)
    //fetch the size of the JanusGraph object from the custom resource
    replicas := m.Spec.Size
    //fetch the version of JanusGraph to install from the custom resource
    version := m.Spec.Version

    //create StatefulSet
    statefulSet := &appsv1.StatefulSet{
        ObjectMeta: metav1.ObjectMeta{
            Name:      m.Name,
            Namespace: m.Namespace,
        },
        Spec: appsv1.StatefulSetSpec{
            Replicas: &replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: ls,
            },
            ServiceName: m.Name + "-service",
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: ls,
                    Name:   "janusgraph",
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Image: "horeaporutiu/janusgraph:" + version,
                            Name:  "janusgraph",
                            Ports: []corev1.ContainerPort{
                                {
                                    ContainerPort: 8182,
                                    Name:          "janusgraph",
                                },
                            },
                        },
                    },
                },
            },
        },
    }
    ctrl.SetControllerReference(m, statefulSet, r.Scheme)
    return statefulSet

Note that this is where you grab the values from the Spec section of your CR. These determine how many pods to create and which version of JanusGraph to deploy.

replicas := m.Spec.Size
version := m.Spec.Version

Now, let’s compare this Golang implementation of a Kubernetes StatefulSet with a similar YAML implementation. Take some time to analyze the picture below — a lot of Kubernetes resources are written in YAML, so it’s useful to be familiar with YAML syntax as well.

operator structure

  1. You set the metadata in much the same way that you did when you created the service.
  2. You set the number of replicas you want by using the value that is entered in the CR. You use another selector, this time MatchLabels, which works in a very similar fashion to the selector you used for the service.
  3. Next, you specify the Docker image you want to use for your container, and the name of the container. In this case, you are using a modified version of the JanusGraph Docker image called horeaporutiu/jansugraph. It’s specifically modified to enable it to run on OpenShift.
  4. You set the containerPort, which is the port that is exposed on the pod’s IP address.

7. Update the user and the CR

Now, go ahead and login to your OpenShift cluster. You can follow the steps described in the tutorial “Build and deploy a basic operator.” After you’ve logged in, go ahead and create a new project:

oc new-project janusgraph-demo-project
Now using project "janusgraph-demo-project" on server "https://c116-e.us-south.containers.cloud.ibm.com:31047".

Edit the CR

Next, let’s create the CR.

Update your CR by modifying config/samples/:

graph_v1alpha1_janusgraph.yaml file to look like this:
apiVersion: graph.example.com/v1alpha1
kind: Janusgraph
metadata:
  name: janusgraph-sample
spec:
  # Add fields here
  size: 1
  version: latest

In the above code, you set the replicas to 1, and the version to latest. You use kubectl to create this CR as part of a the build-and-deploy-janus.sh script.

Open up the editor of your choice and create a new file called build-and-deploy-janus.sh script with the following code (you can view a copy of the code in GitHub):

set -x
set -e

make generate
make manifests
make install

export namespace=<add-namespace-here>

export img=docker.io/<username-goes-here>/janusgraph-operator:latest

cd config/manager
kustomize edit set namespace $namespace
kustomize edit set image controller=$img
cd ../../
cd config/default
kustomize edit set namespace $namespace
cd ../../

make docker-build IMG=$img
make docker-push IMG=$img
make deploy IMG=$img

kubectl apply -f config/samples/graph_v1alpha1_janusgraph.yaml

Note that you need to edit the two export statements.

  1. Add in your namespace. This is the name of your OpenShift project where you plan to deploy your operator.
  2. Add in your username for your image registry (such as Docker Hub, or another registry where you push your images to).

Once you save the file after editing the export statements, it should look something like this:

set -x
set -e

img="horeaporutiu/janusgraph-operator:latest"
namespace="janusgraph-demo-project"

cd config/manager
kustomize edit set namespace $namespace
kustomize edit set image controller=$img
cd ../../
cd config/default
kustomize edit set namespace $namespace
cd ../../

make docker-build IMG=$img
make docker-push IMG=$img
make deploy IMG=$img

kubectl apply -f config/samples/graph_v1alpha1_janusgraph.yaml

Now you can run the script. To ensure that the script has the correct access control, you can run the following:

chmod 777 build-and-deploy-janus.sh.

8. Build, push, and deploy your operator

Note: Make sure you are logged into your image registry, such as your Docker Hub account. Otherwise, the script may fail since you need to push your image to a registry.

It’s finally time to build and deploy your operator! Let’s do so by running the following script:

build-and-deploy-janus.sh

Once the script has finished running successfully, you will see something like this:

deployment.apps/janusgraph-operator-controller-manager created
+ kubectl apply -f config/samples/graph_v1alpha1_janusgraph.yaml
janusgraph.graph.example.com/janusgraph-sample created

To make sure everything is working correctly, use the oc get pods command:

oc get pods

NAME                                                     READY   STATUS    RESTARTS   AGE
janusgraph-operator-controller-manager-54c5864f7b-znwws   2/2     Running   0          14s
janusgraph-sample-0                                       1/1     Running   0          5s

This means your operator is up and running. Great job!

9. Verify the operator

From the terminal, run kubectl get all or oc get all to make sure that the controllers, managers, and pods have been successfully created and are in Running state with the right number of pods as defined in the spec.

kubectl get all

You should see one janusgraph-sample pod running.

Load and retrieve data from JanusGraph using the Gremlin console

Now that you have your JanusGraph application running in a pod, you can test it to make sure it works as expected. Go to the next JanusGraph tutorial to see the steps you need to take to test your JanusGraph application.

You’ll use the data file in the data directory from this repo, so you may first need to clone the repo.

Once you reach the bottom of step 4 of the next tutorial, you should be able to list all of your data, and you should get a response like the following:

gremlin> g.V().has("object_type", "flight").limit(30000).values("airlines").dedup().toList()
==>MilkyWay Airlines
==>Spartan Airlines
==>Phoenix Airlines

If you get the result shown above, then great job! You’re finished testing your JanusGraph application.

Conclusion

Congratulations! You’ve just created a Level I operator for JanusGraph using the default BerkeleyDB configuration. In the next tutorial, you will learn how to create a more complex Level I operator for JanusGraph using Cassandra as the backend storage. You will also see how to scale the JanusGraph application up and down using the CR.