REST Microservices with Gin

Introduction

This post gives an overview with example to write RESTful API services in Golang.

It used Gin, Cassandra.

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster.

The setup contains 1 node hosting both Cassandra and Golang service

Services in Go

Schema

Routes

Let us first define the routes (REST APIs)

[Create] POST : http://127.0.0.1:8080/api/v1/policies

[Read] GET : http://127.0.0.1:8080/api/v1/policies

[Read] GET : http://127.0.0.1:8080/api/v1/policies/1

[Update] PUT : http://127.0.0.1:8080/api/v1/policies/1

[Delete] DELETE : http://127.0.0.1:8080/api/v1/policies/1

Table

Let's also define the table structure to store the policies blob

Table 'policies'

id — big_int(20) & auto-increment

policy — varchar(255)

Gin for Routes

We’re gonna use Gin who is a micro framework routing. It’s friendly easy and fast.

go get github.com/gin-gonic/gin

Code

Ok, let’s start coding ! Firstly in a new “main.go” file, we call our libraries.

package main

import

(

"github.com/gin-gonic/gin"

"strconv"

"github.com/hashicorp/consul/api"

)

Secondly, we declare the structure “Policy” :

type User struct {

Id int64 `db:"id" json:"id"`

Policy string `db:"policy" json:"policy"`

}

The “db” param will be used later, with the database connection…

Thirdly, in the main() function, we regoup our routes in a single group :

func main() {

r := gin.Default()

v1 := r.Group("api/v1")

{

v1.GET("/policies", GetPolicies)

v1.GET("/policies/:id", GetPolicies)

v1.POST("/policies", PostPolicy)

v1.PUT("/policies/:id", UpdatePolicy)

v1.DELETE("/policies/:id", DeletePolicy)

}

r.Run(":8080")

}

Then we declare the five functions calling in the routes

Deployment

Create a VMa with Redhat flavor CentOS 7.1

Cassandra Node Setup

Install Java

$ sudo yum -y install java-1.8.0-openjdk

$ java -version

openjdk version "1.8.0_65"

OpenJDK Runtime Environment (build 1.8.0_65-b17)

OpenJDK 64-Bit Server VM (build 25.65-b01, mixed mode)

Install Cassandra

$ wget http://www.carfab.com/apachesoftware/cassandra/2.2.4/apache-cassandra-2.2.4-bin.tar.gz

$ tar -xzvf apache-cassandra-2.2.4-bin.tar.gz

$ cd apache-cassandra-2.2.4

Configure Cassandra

$ sudo mkdir /opt/lib

$ sudo mkdir /opt/lib/cassandra/

$ sudo mkdir /opt/lib/cassandra/data

$ sudo chmod 777 /opt/lib/cassandra/ -R

$ cd conf/

$ cp cassandra.yaml cassandra.yaml.orig

$vi cassandra.yaml

Old

# The name of the cluster. This is mainly used to prevent machines in

# one logical cluster from joining another.

cluster_name: 'Test Cluster'

New

cluster_name: 'CRUD-Cluster'

Old\New - Leave this as it is

# If you already have a cluster with 1 token per node, and wish to migrate to

# multiple tokens per node, see http://wiki.apache.org/cassandra/Operations

num_tokens: 256

Old

# - AllowAllAuthenticator performs no checks - set it to disable authentication.

# - PasswordAuthenticator relies on username/password pairs to authenticate

# users. It keeps usernames and hashed passwords in system_auth.credentials table.

# Please increase system_auth keyspace replication factor if you use this authenticator.

# If using PasswordAuthenticator, CassandraRoleManager must also be used (see below)

authenticator: AllowAllAuthenticator

New

authenticator: PasswordAuthenticator (Recommended)

authenticator: AllowAllAuthenticator

Old

# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.

# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please

# increase system_auth keyspace replication factor if you use this authorizer.

authorizer: AllowAllAuthorizer

New

authorizer: CassandraAuthorizer (Recommended)

authorizer: AllowAllAuthorizer

Old\New - Use existing only

# Besides Murmur3Partitioner, partitioners included for backwards

# compatibility include RandomPartitioner, ByteOrderedPartitioner, and

# OrderPreservingPartitioner.

#

partitioner: org.apache.cassandra.dht.Murmur3Partitioner

Old

# Directories where Cassandra should store data on disk. Cassandra

# will spread data evenly across them, subject to the granularity of

# the configured compaction strategy.

# If not set, the default directory is $CASSANDRA_HOME/data/data.

# data_file_directories:

# - /var/lib/cassandra/data

New - Ensure no-space before data_file_directories

# If not set, the default directory is $CASSANDRA_HOME/data/data.

data_file_directories:

- /opt/lib/cassandra/data

Old

# commit log. when running on magnetic HDD, this should be a

# separate spindle than the data directories.

# If not set, the default directory is $CASSANDRA_HOME/data/commitlog.

# commitlog_directory: /var/lib/cassandra/commitlog

New - Ensure no-space before commlilog

# If not set, the default directory is $CASSANDRA_HOME/data/commitlog.

# commitlog_directory: /var/lib/cassandra/commitlog

commitlog_directory: /opt/lib/cassandra/commitlog

Old

saved caches

# If not set, the default directory is $CASSANDRA_HOME/data/saved_caches.

# saved_caches_directory: /var/lib/cassandra/saved_caches

New

# saved_caches_directory: /var/lib/cassandra/saved_caches

saved_caches_directory: /opt/lib/cassandra/saved_caches

Old\New - No change for now, need to change for cluster

# any class that implements the SeedProvider interface and has a

# constructor that takes a Map<String, String> of parameters will do.

seed_provider:

# Addresses of hosts that are deemed contact points.

# Cassandra nodes use this list of hosts to find each other and learn

# the topology of the ring. You must change this if you are running

# multiple nodes!

- class_name: org.apache.cassandra.locator.SimpleSeedProvider

parameters:

# seeds is actually a comma-delimited list of addresses.

# Ex: "<ip1>,<ip2>,<ip3>"

- seeds: "127.0.0.1"

# If IP based cqlsh access - seeds: "127.0.0.1,192.168.0.164"

Old

# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.

listen_address: localhost

New

# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.

listen_address: 192.168.0.164

#If IP based cqlsh access listen_address: 192.168.0.164

Old

# If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address

# you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4

# address will be used. If true the first ipv6 address will be used. Defaults to false preferring

# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.

rpc_address: localhost

New

# ipv4. If there is only one address it will be selected regardless of ipv4/ipv6.

rpc_address: 192.168.0.164

# If IP based cqlsh access rpc_address: 192.168.0.164

Note that unlike listen_address, you can specify 0.0.0.0, but you must also

# set broadcast_rpc_address to a value other than 0.0.0.0.

# Also set broadcast_rpc_address: 1.2.3.4, if rpc_address is set to 0.0.0.0

Old\New - Same, In production use GossipingPropertyFileSnitch, single node use simplesnitch

# You can use a custom Snitch by setting this to the full class name

# of the snitch, which will be assumed to be on your classpath.

endpoint_snitch: SimpleSnitch

if use GossipingPropertyFileSnitch, chance as per your topology

# vi cassandra-rackdc.properties

# These properties are used with GossipingPropertyFileSnitch and will

# indicate the rack and dc for this node

dc=DC1

rack=RAC1

Disable firewall

sudo systemctl disable firewalld

sudo systemctl stop firewalld

sudo systemctl status firewalld

Start Cassandra

$ cd /home/centos/bin/cassandra/apache-cassandra-2.2.3/bin/

$ ./cassandra &

Check cassandra

ps auwx | grep cassandra

Cassandra Tools

$ cd /home/centos/bin/cassandra/apache-cassandra-2.2.3/bin/

$ ./nodetool help

$ ./cqlsh --help

$ ./nodetool version

ReleaseVersion: 2.2.3

$ ./nodetool status

Datacenter: datacenter1

=======================

Status=Up/Down

|/ State=Normal/Leaving/Joining/Moving

-- Address Load Tokens Owns Host ID Rack

UN 127.0.0.1 131.31 KB 256 ? 39fc1d89-ea3d-45aa-ab1c-68139c76945d rack1

Cassandra Create Table

$ ./cqlsh [192.168.0.164 -u cassandra -p cassandra] # Default user\password not required if authenticator and authorizor is Allow All

$ ./cqlsh

Connected to CRUD-Cluster at 127.0.0.1:9042.

[cqlsh 5.0.1 | Cassandra 2.2.4 | CQL spec 3.3.1 | Native protocol v4]

Use HELP for help.

cqlsh>

Before you execute the program, Launch `cqlsh` and execute:

> create keyspace demo with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };

> use demo;

> create table demo.policy(class text, name text, blob text, PRIMARY KEY(class, name));

This is not required as class, name is already part of primary key.

> create index on demo.policy(class);

> create index on demo.policy(name);

cqlsh> use demo;

cqlsh:demo> desc policy;

CREATE TABLE demo.policy (

class text,

name text,

blob text,

PRIMARY KEY (class, name)

) WITH CLUSTERING ORDER BY (name ASC)

AND bloom_filter_fp_chance = 0.01

AND caching = '{"keys":"ALL", "rows_per_partition":"NONE"}'

AND comment = ''

AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy'}

AND compression = {'sstable_compression': 'org.apache.cassandra.io.compress.LZ4Compressor'}

AND dclocal_read_repair_chance = 0.1

AND default_time_to_live = 0

AND gc_grace_seconds = 864000

AND max_index_interval = 2048

AND memtable_flush_period_in_ms = 0

AND min_index_interval = 128

AND read_repair_chance = 0.0

AND speculative_retry = '99.0PERCENTILE';

cqlsh:demo> select * from policy;

class | name | blob

--------+------------+-------

common | email-sign | rule5

common | email-web | rule6

(2 rows)

Start Consul Server [Optional for Consul Service Discovery]

The consul is used for service discovery of cassandra by CRUD service.

Download consul

$ cd /home/centos

$ wget https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip

$ sudo yum -y install unzip

$ unzip 0.5.2_linux_amd64.zip

$ mkdir /tmp/consul

$ sudo mkdir /etc/consul.d

$ ./consul agent -server -bootstrap-expect=1 -data-dir /tmp/consul -config-dir /etc/consul.d -node=consul-server-db --bind=192.168.0.158 &

$ ./consul members

2015/12/18 19:16:52 [INFO] agent.rpc: Accepted client: 127.0.0.1:58388

Node Address Status Type Build Protocol DC

consul-server-db 192.168.0.158:8301 alive server 0.5.2 2 dc1

Register Cassandra Service in Consul [Optional for Consul Service Discovery]

curl -X PUT http://localhost:8500/v1/agent/service/register -d '{"ID":"cassandra", "name":"cassandra","tags": ["rails"], "port": 9160, "Address":"192.168.0.158"}'

$ curl http://localhost:8500/v1/catalog/service/cassandra | python -m json.tool

% Total % Received % Xferd Average Speed Time Time Time Current

Dload Upload Total Spent Left Speed

100 181 100 181 0 0 1180 0 --:--:-- --:--:-- --:--:-- 1183

[

{

"Address": "192.168.0.158",

"Node": "consul-server-db",

"ServiceAddress": "192.168.0.158",

"ServiceID": "cassandra",

"ServiceName": "cassandra",

"ServicePort": 9160,

"ServiceTags": [

"rails"

]

}

]

CRUD-Server Node Setup

Install Golang

$ sudo yum -y update

$ sudo yum -y install golang

$ go version

go version go1.4.2 linux/amd64

Install gin\gocql\api for Routes

$ go help gopath

$ mkdir /home/centos/repos

$ export GOPATH=/home/centos/repos/

$ go get github.com/gin-gonic/gin

$ go get github.com/gocql/gocql

$ go get github.com/hashicorp/consul/api

Create directory structure for Go service

$ cd /home/centos/repos

$ mkdir crud-service

$ mkdir crud-service/crud-server

$ mkdir crud-service/crud-client

$ mkdir crud-service/crud-test

$ mkdir crud-service/crud-server/bin

$ mkdir crud-service/crud-server/pkg

$ mkdir crud-service/crud-server/src

$ mkdir crud-service/crud-client/bin

$ mkdir crud-service/crud-client/pkg

$ mkdir crud-service/crud-client/src

$ mkdir crud-service/crud-test/bin

$ mkdir crud-service/crud-test/pkg

$ mkdir crud-service/crud-test/src

Create the service main.go

/home/centos/repos/crud-service/crud-server/src/main.go

main.go

cat src/main.go

package main

import

(

"fmt"

"log"

"github.com/gin-gonic/gin"

// "strconv"

"github.com/gocql/gocql"

// "github.com/hashicorp/consul/api"

)

type Policy struct {

Class string `db:"class" json:"class"`

Name string `db:"name" json:"name"`

Blob string `db:"blob" json:"blob"`

}

/* Before you execute the program, Launch `cqlsh` and execute:

create keyspace demo with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };

create table demo.policy(class text, name text, blob text, PRIMARY KEY(class, name));

// Being primary key, below won;t work

create index on demo.policy(class);

create index on demo.policy(name);

curl -X PUT http://localhost:8500/v1/agent/service/register -d '{"ID":"cassandra", "name":"cassandra","tags": ["rails"], "port": 9160, "Address":"192.168.0.168"}'

curl http://localhost:8500/v1/catalog/service/cassandra | python -m json.tool

*/

func checkErr(err error, msg string) {

if err != nil {

log.Fatalln(msg, err)

}

}

func main() {

r := gin.Default()

v1 := r.Group("api/v1")

{

v1.GET("/policies/:class", GetPolicies)

v1.GET("/policies/:class/:name", GetPolicy)

v1.POST("/policies", PostPolicy)

v1.PUT("/policies/:class/:name", UpdatePolicy)

v1.DELETE("/policies/:class/:name", DeletePolicy)

}

r.Run(":8080")

}

func GetCassandraIP() (IP string) {

//var IP string

/*

//Get a new client

client, err := api.NewClient(api.DefaultConfig())

if err != nil {

panic(err)

}

agent := client.Agent()

services, err := agent.Services()

fmt.Println("services : ", services)

if err != nil {

log.Fatal("err: %v", err)

}

if _, ok := services["cassandra"]; !ok {

log.Fatal("missing service: %v", services)

}

IP = services["cassandra"].Address

fmt.Println(" Service cassandra IP : ", services["cassandra"].Address)

*/

IP = "127.0.0.1"

fmt.Println(" Service cassandra IP : ", IP)

return

}

// CRUD Operations

// READ all policies

func GetPolicies(c *gin.Context) {

// connect to the cluster

cluster := gocql.NewCluster(GetCassandraIP())

// A keyspace in Cassandra is a namespace that defines data replication on nodes. A cluster contains one keyspace per node.

cluster.Keyspace = "demo"

// May use gocql.Quorum

cluster.Consistency = gocql.LocalOne

session, _ := cluster.CreateSession()

// Make sure that the connection can close once you are done.

defer session.Close()

// ####################################### Query Logic ##########################################

class := c.Params.ByName("class")

fmt.Println("class : ", class)

var policies []Policy

var policy Policy

iter := session.Query("SELECT class, name, blob FROM policy WHERE class=?", class).Iter()

for iter.Scan(&policy.Class, &policy.Name, &policy.Blob) {

// fmt.Println("1 : ", policy.Class, policy.Name, policy.Blob)

policies = append(policies, policy)

}

if len(policies) > 0 {

c.JSON(200, policies)

} else {

c.JSON(404, gin.H{"error": "no policy(s) into the table"})

}

if err := iter.Close(); err != nil {

log.Fatal(err)

}

// curl -i http://localhost:8080/api/v1/policies/common

}

// Read a user

func GetPolicy(c *gin.Context) {

// connect to the cluster

cluster := gocql.NewCluster(GetCassandraIP())

// A keyspace in Cassandra is a namespace that defines data replication on nodes. A cluster contains one keyspace per node.

cluster.Keyspace = "demo"

// May use gocql.Quorum

cluster.Consistency = gocql.LocalOne

session, _ := cluster.CreateSession()

// Make sure that the connection can close once you are done.

defer session.Close()

// ####################################### Query Logic ##########################################

class := c.Params.ByName("class")

name := c.Params.ByName("name")

var policy Policy

err := session.Query("SELECT class, name, blob FROM policy WHERE class=? AND name=?", class, name).Scan(&policy.Class, &policy.Name, &policy.Blob);

if err == nil {

c.JSON(200, policy)

} else {

c.JSON(404, gin.H{"error": "no policy(s) found"})

}

// curl -i http://localhost:8080/api/v1/policies/common/email

}

// Create a user

func PostPolicy(c *gin.Context) {

// connect to the cluster

cluster := gocql.NewCluster(GetCassandraIP())

// A keyspace in Cassandra is a namespace that defines data replication on nodes. A cluster contains one keyspace per node.

cluster.Keyspace = "demo"

// May use gocql.Quorum

cluster.Consistency = gocql.LocalOne

session, _ := cluster.CreateSession()

// Make sure that the connection can close once you are done.

defer session.Close()

// ####################################### Query Logic ##########################################

var policy Policy

if c.BindJSON(&policy) == nil {

fmt.Println("Bind success", c.Params.ByName("Class"))

} else {

fmt.Println("Bind failure")

}

fmt.Println("Policy: ", policy.Class, policy.Name, policy.Blob)

if policy.Class != "" && policy.Name != "" && policy.Blob != "" {

err := session.Query("INSERT INTO policy (class, name, blob) VALUES (?, ?, ?)", policy.Class, policy.Name, policy.Blob).Exec()

if err == nil {

c.JSON(201, policy)

/* // Commenting consul KV store

// Get a new client

client, err := api.NewClient(api.DefaultConfig())

if err != nil {

panic(err)

}

// Get a handle to the KV API

kv := client.KV()

key := "notify" + "/" + "policy" + "/" + policy.Class + "/" + policy.Name

value := []byte("POST")

fmt.Println(key, " : ", value)

// PUT a new KV pair

p := &api.KVPair{Key: key, Value: value}

_, err = kv.Put(p, nil)

if err != nil {

panic(err)

} else {

fmt.Println("kv updated")

}

*/

} else {

checkErr(err, "Insert failed")

}

} else {

c.JSON(422, gin.H{"error": "fields are empty"})

}

// curl -i -X POST -H "Content-Type: application/json" -d "{ \"class\": \"common\", \"name\": \"email\", \"blob\": \"{rule-2}\" }" http://localhost:8080/api/v1/policies

}

// Update a user

func UpdatePolicy(c *gin.Context) {

// connect to the cluster

cluster := gocql.NewCluster(GetCassandraIP())

// A keyspace in Cassandra is a namespace that defines data replication on nodes. A cluster contains one keyspace per node.

cluster.Keyspace = "demo"

// May use gocql.Quorum

cluster.Consistency = gocql.LocalOne

session, _ := cluster.CreateSession()

// Make sure that the connection can close once you are done.

defer session.Close()

// ####################################### Query Logic ##########################################

fmt.Println("In Update")

var json Policy

c.BindJSON(&json)

json.Class = c.Params.ByName("class")

json.Name = c.Params.ByName("name")

fmt.Println("json : ", json.Class, json.Name, json.Blob)

var policy Policy

err := session.Query("SELECT class, name, blob FROM policy WHERE class=? AND name=?", json.Class, json.Name).Scan(&policy.Class, &policy.Name, &policy.Blob);

if err == nil {

fmt.Println("select query success ")

if json.Blob != "" {

err := session.Query("UPDATE policy SET blob=? WHERE class=? AND name=?", json.Blob, json.Class, json.Name).Exec();

if err == nil {

c.JSON(200, json)

/* // Commenting consul KV

// Get a new client

client, err := api.NewClient(api.DefaultConfig())

if err != nil {

panic(err)

}

// Get a handle to the KV API

kv := client.KV()

key := "notify" + "/" + "policy" + "/" + json.Class + "/" + json.Name

value := []byte("PUT")

fmt.Println(key, " : ", value)

// PUT a new KV pair

p := &api.KVPair{Key: key, Value: value}

_, err = kv.Put(p, nil)

if err != nil {

panic(err)

} else {

fmt.Println("kv updated")

}

*/

} else {

checkErr(err, "Update failed")

}

} else {

c.JSON(422, gin.H{"error": "fields are empty"})

}

} else {

fmt.Println("select query failed")

c.JSON(404, gin.H{"error": "policy not found"})

}

// curl -i -X PUT -H "Content-Type: application/json" -d "{ \"blob\": \"{rule-2}\" }" http://localhost:8080/api/v1/policies/common/email

}

// Delete a user

func DeletePolicy(c *gin.Context) {

// connect to the cluster

cluster := gocql.NewCluster(GetCassandraIP())

// A keyspace in Cassandra is a namespace that defines data replication on nodes. A cluster contains one keyspace per node.

cluster.Keyspace = "demo"

// May use gocql.Quorum

cluster.Consistency = gocql.LocalOne

session, _ := cluster.CreateSession()

// Make sure that the connection can close once you are done.

defer session.Close()

// ####################################### Query Logic ##########################################

class := c.Params.ByName("class")

name := c.Params.ByName("name")

var policy Policy

err := session.Query("SELECT class, name, blob FROM policy WHERE class=? AND name=?", class, name).Scan(&policy.Class, &policy.Name, &policy.Blob);

if err == nil {

if err := session.Query("DELETE FROM policy WHERE class=? AND name=?", class, name).Exec(); err != nil {

log.Fatal(err)

}

if err == nil {

c.JSON(200, policy)

/* // Commented consul KV

// Get a new client

client, err := api.NewClient(api.DefaultConfig())

if err != nil {

panic(err)

}

// Get a handle to the KV API

kv := client.KV()

key := "notify" + "/" + "policy" + "/" + policy.Class + "/" + policy.Name

value := []byte("DELETE")

fmt.Println(key, " : ", value)

// PUT a new KV pair

p := &api.KVPair{Key: key, Value: value}

_, err = kv.Put(p, nil)

if err != nil {

panic(err)

} else {

fmt.Println("kv updated")

}

*/

} else {

checkErr(err, "Delete failed")

}

} else {

c.JSON(404, gin.H{"error": "policy not found"})

}

// curl -i -X DELETE http://localhost:8080/api/v1/policies/common/email

}

Disable firewall

sudo systemctl disable firewalld

sudo systemctl stop firewalld

sudo systemctl status firewalld

Start Consul Client [Optional for Consul Service Discovery]

The consul is used for service discovery of cassandra by CRUD service.

Download consul

$ cd /home/centos

$ wget https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip

$ sudo yum -y install unzip

$ unzip 0.5.2_linux_amd64.zip

$ mkdir /tmp/consul

$ sudo mkdir /etc/consul.d

$ ./consul agent -data-dir /tmp/consul -config-dir /etc/consul.d -node=consul-server-db --bind=192.168.0.164 &

$ ./consul join 192.168.0.164 (IP of Cassandra Node)

$ ./consul members

2015/12/18 19:22:27 [INFO] agent.rpc: Accepted client: 127.0.0.1:53227

Node Address Status Type Build Protocol DC

consul-server-crud 192.168.0.164:8301 alive client 0.5.2 2 dc1

consul-server-db 192.168.0.158:8301 alive server 0.5.2 2 dc1

Start the CRUD service

$ cd /home/centos/repos/crud-service/crud-server/src/

$ go run src/main.go

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.

- using env: export GIN_MODE=release

- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET /api/v1/policies/:class --> main.GetPolicies (3 handlers)

[GIN-debug] GET /api/v1/policies/:class/:name --> main.GetPolicy (3 handlers)

[GIN-debug] POST /api/v1/policies --> main.PostPolicy (3 handlers)

[GIN-debug] PUT /api/v1/policies/:class/:name --> main.UpdatePolicy (3 handlers)

[GIN-debug] DELETE /api/v1/policies/:class/:name --> main.DeletePolicy (3 handlers)

[GIN-debug] Listening and serving HTTP on :8080

CRUD-Client Node Setup

Create a VM say 192.168.0.168

Access the CRUD service REST interface and validate.

Read Operation (DB empty)

$ curl -i http://192.168.0.164:8080/api/v1/policies/common

HTTP/1.1 404 Not Found

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:30:00 GMT

Content-Length: 40

{"error":"no policy(s) into the table"}

Server Log:

[GIN] 2015/12/18 - 21:00:00 | 404 | 126.750701ms | 192.168.0.168:50952 | GET /api/v1/policies/common

$ curl -i http://192.168.0.164:8080/api/v1/policies/common/email

HTTP/1.1 404 Not Found

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:30:48 GMT

Content-Length: 31

{"error":"no policy(s) found"}

Server Log:

[GIN] 2015/12/18 - 21:00:48 | 404 | 91.628026ms | 192.168.0.168:51389 | GET /api/v1/policies/common/email

Create Operation

$ curl -i -X POST -H "Content-Type: application/json" -d "{ \"class\": \"common\", \"name\": \"email-sign\", \"blob\": \"rule5\" }" http://192.168.0.164:8080/api/v1/policies

HTTP/1.1 201 Created

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:33:03 GMT

Content-Length: 54

{"class":"common","name":"email-sign","blob":"rule5"}

Server Log:

[GIN] 2015/12/18 - 21:08:51 | 201 | 72.241961ms | 192.168.0.168:55797 | POST /api/v1/policies

$ curl -i http://192.168.0.164:8080/api/v1/policies/common

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:33:11 GMT

Content-Length: 56

[{"class":"common","name":"email-sign","blob":"rule5"}]

$ curl -i -X POST -H "Content-Type: application/json" -d "{ \"class\": \"common\", \"name\": \"email-web\", \"blob\": \"rule6\" }" http://192.168.0.164:8080/api/v1/policies

$ curl -i http://192.168.0.164:8080/api/v1/policies/common

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:39:45 GMT

Content-Length: 109

[{"class":"common","name":"email-sign","blob":"rule5"},{"class":"common","name":"email-web","blob":"rule6"}]

Update Operation

$ curl -i -X PUT -H "Content-Type: application/json" -d "{ \"blob\": \"{rule-2}\" }" http://192.168.0.164:8080/api/v1/policies/common/email-sign

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:41:19 GMT

Content-Length: 57

{"class":"common","name":"email-sign","blob":"{rule-2}"}

Server Log:

[GIN] 2015/12/18 - 21:11:19 | 200 | 63.486158ms | 192.168.0.168:57144 | PUT /api/v1/policies/common/email-sign

$ curl -i http://192.168.0.164:8080/api/v1/policies/common

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:42:21 GMT

Content-Length: 112

[{"class":"common","name":"email-sign","blob":"{rule-2}"},{"class":"common","name":"email-web","blob":"rule6"}]

$ curl -i http://192.168.0.164:8080/api/v1/policies/common/email-sign

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:42:34 GMT

Content-Length: 57

{"class":"common","name":"email-sign","blob":"{rule-2}"}

Delete Operation

$ curl -i -X DELETE http://192.168.0.164:8080/api/v1/policies/common/email-sign

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:43:38 GMT

Content-Length: 57

Server Log:

[GIN] 2015/12/18 - 21:13:38 | 200 | 81.548881ms | 192.168.0.168:58409 | DELETE /api/v1/policies/common/email-sign

{"class":"common","name":"email-sign","blob":"{rule-2}"}

$ curl -i http://192.168.0.164:8080/api/v1/policies/common

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Date: Fri, 18 Dec 2015 15:43:44 GMT

Content-Length: 55

[{"class":"common","name":"email-web","blob":"rule6"}]

Push the CRUD service into Git Repository

Create a repo

https://github.com/deepagargit/crud-service

Clone the repo

git clone https://github.com/deepagargit/crud-service

Create new git branch

$ git checkout -b develop

Switched to a new branch 'develop'

Copy the file in repo

$ ls -R

.:

commands.sh common crud-client crud-server crud-test run.sh

./common:

commands.cql

./crud-client:

bin pkg src

./crud-client/bin:

./crud-client/pkg:

./crud-client/src:

./crud-server:

bin pkg src

./crud-server/bin:

./crud-server/pkg:

./crud-server/src:

main.go

./crud-test:

bin pkg src

./crud-test/bin:

./crud-test/pkg:

./crud-test/src:

$ cat run.sh

#!/usr/bin/env bash

cd $(dirname $0)

go run crud-server/src/main.go

$ cat commands.sh

#!/usr/bin/env bash

cd $(dirname $0)

sleep 10s

cqlsh -f common/commands.cql

$ cat common/commands.cql

create keyspace demo with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };

create table demo.policy(class text, name text, blob text, PRIMARY KEY(class, name));

Add the files to Git

Touch the gitkeep file in empty folder.

touch .gitkeep

git add .

$ git status

# On branch develop

#

# Initial commit

#

# Changes to be committed:

# (use "git rm --cached <file>..." to unstage)

#

# new file: commands.sh

# new file: common/commands.cql

# new file: crud-client/.gitkeep

# new file: crud-client/bin/.gitkeep

# new file: crud-client/pkg/.gitkeep

# new file: crud-client/src/.gitkeep

# new file: crud-server/bin/.gitkeep

# new file: crud-server/pkg/.gitkeep

# new file: crud-server/src/main.go

# new file: crud-test/.gitkeep

# new file: crud-test/bin/.gitkeep

# new file: crud-test/pkg/.gitkeep

# new file: crud-test/src/.gitkeep

# new file: run.sh

#

Add User Config

git config --global user.email "deepak.aagarwal@gmail.com"

git config --global user.name "Deepak Agarwal"

Commit the files

$ git commit -m "Added CRUD service with Golang"

[develop (root-commit) 7611268] Added CRUD service with Golang

14 files changed, 432 insertions(+)

create mode 100755 commands.sh

create mode 100644 common/commands.cql

create mode 100644 crud-client/.gitkeep

create mode 100644 crud-client/bin/.gitkeep

create mode 100644 crud-client/pkg/.gitkeep

create mode 100644 crud-client/src/.gitkeep

create mode 100644 crud-server/bin/.gitkeep

create mode 100644 crud-server/pkg/.gitkeep

create mode 100644 crud-server/src/main.go

create mode 100644 crud-test/.gitkeep

create mode 100644 crud-test/bin/.gitkeep

create mode 100644 crud-test/pkg/.gitkeep

create mode 100644 crud-test/src/.gitkeep

create mode 100755 run.sh

Push to the develop branch

$ git push origin develop

Username for 'https://github.com': deepagargit

Password for 'https://deepagargit@github.com':

Counting objects: 12, done.

Delta compression using up to 16 threads.

Compressing objects: 100% (8/8), done.

Writing objects: 100% (12/12), 3.21 KiB | 0 bytes/s, done.

Total 12 (delta 0), reused 0 (delta 0)

To https://github.com/deepagargit/crud-service

* [new branch] develop -> develop

References

Cassandra