Initial version with full functionality 15/15/4
authorAbukar Mohamed <abukar.mohamed@nokia.com>
Wed, 3 Apr 2019 11:07:48 +0000 (11:07 +0000)
committerAbukar Mohamed <abukar.mohamed@nokia.com>
Thu, 4 Apr 2019 08:47:40 +0000 (08:47 +0000)
Appmgr (aka Xapp-Manager) supports following:
- Deploying, querying and undeploying Xapps
- Health check for Kubernates readiness vs liveness probes
- Subscriptions (resthooks) & notification of Xapp action

Interfaces:
- RestApi
- Helm
- SDL

Change-Id: I4ce6aec5fd581c4fac6aaa709deb1eb3850ba318
Signed-off-by: Abukar Mohamed <abukar.mohamed@nokia.com>
26 files changed:
LICENSES.txt [new file with mode: 0755]
README.md [new file with mode: 0755]
build/Dockerfile [new file with mode: 0755]
build/Makefile [new file with mode: 0755]
build/docker-entrypoint.sh [new file with mode: 0755]
cli/appmgrcli [new file with mode: 0755]
config/appmgr.yaml [new file with mode: 0755]
config/msg_type.yaml [new file with mode: 0755]
helm_chart/appmgr/Chart.yaml [new file with mode: 0755]
helm_chart/appmgr/templates/_helpers.tpl [new file with mode: 0755]
helm_chart/appmgr/templates/appconfig.yaml [new file with mode: 0755]
helm_chart/appmgr/templates/appenv.yaml [new file with mode: 0755]
helm_chart/appmgr/templates/deployment.yaml [new file with mode: 0755]
helm_chart/appmgr/templates/service.yaml [new file with mode: 0755]
helm_chart/appmgr/values.yaml [new file with mode: 0755]
rest_api/xapp_manager_rest_api.json [new file with mode: 0644]
src/api.go [new file with mode: 0755]
src/api_test.go [new file with mode: 0755]
src/config.go [new file with mode: 0755]
src/helm.go [new file with mode: 0755]
src/helm_test.go [new file with mode: 0755]
src/logger.go [new file with mode: 0755]
src/main.go [new file with mode: 0755]
src/subscriptions.go [new file with mode: 0755]
src/subscriptions_test.go [new file with mode: 0755]
src/types.go [new file with mode: 0755]

diff --git a/LICENSES.txt b/LICENSES.txt
new file mode 100755 (executable)
index 0000000..5c81246
--- /dev/null
@@ -0,0 +1,28 @@
+Unless otherwise specified, all software contained herein is licensed
+under the Apache License, Version 2.0 (the "Software License");
+you may not use this software except in compliance with the Software
+License. You may obtain a copy of the Software License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Software License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the Software License for the specific language governing permissions
+and limitations under the Software License.
+
+
+
+Unless otherwise specified, all documentation contained herein is licensed
+under the Creative Commons License, Attribution 4.0 Intl. (the
+"Documentation License"); you may not use this documentation except in
+compliance with the Documentation License. You may obtain a copy of the
+Documentation License at
+
+https://creativecommons.org/licenses/by/4.0/
+
+Unless required by applicable law or agreed to in writing, documentation
+distributed under the Documentation License is distributed on an "AS IS"
+BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Documentation License for the specific language governing
+permissions and limitations under the Documentation License.
diff --git a/README.md b/README.md
new file mode 100755 (executable)
index 0000000..f28f1dc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,390 @@
+# RIC xApp Manager
+
+Provides a flexible and secure way for deploying and managing various RIC xApp applications.
+
+## Communication Interfaces (draft for R0)
+* Northbound (External)
+  * RESTful API
+* Southbound (internal)
+  * Helm  (Package manager for Kubernetes)
+
+## REST services for XApp managements
+```sh
+Action                      URL                                 Method
+
+Deploy                      /ric/v1/xapps                       POST
+Undeploy                    /ric/v1/xapps/{xappName}            DELETE
+Query Xapp Status           /ric/v1/xapps/{xappName}            GET
+Query Xapp Instance Status  /ric/v1/xapps/instances/{xappName}  GET
+Query All Xapp Status       /ric/v1/xapps                       GET
+Health Check                /ric/v1/health                      GET
+```
+
+## REST services for subscriptions (resthooks)
+```sh
+Action                      URL                                 Method
+
+Add A Subscription          /ric/v1/subscriptions               POST
+Update A Subscription       /ric/v1/subscriptions/{id}          PUT
+Delete A Subscription       /ric/v1/subscriptions/{id}          DELETE
+Get A Subscription          /ric/v1/subscriptions               GET
+Get All Subscriptions       /ric/v1/subscriptions/{id}          GET
+```
+
+## Used RIC platform services 
+TBD later
+
+## Prerequisites
+Make sure that following tools are properly installed and configured
+* GO (golang) development and runtime tools
+* Docker
+* Kubernates and related tools (kubectl and helm)
+* Xapp Docker repo (either local or remote)
+* Xapp Helm charts
+* com/log
+
+## Building go binary and docker container for xApp Manager
+ ```sh
+# Change to build-folder and run following command
+make docker-build
+```
+
+## Running xApp Manager unit tests
+ ```sh
+# Change to build-folder and run following command
+make test
+```
+
+## Running xApp Manager locally
+```sh
+# Now run the xApp manager
+./xapp_mgr -f ../config/appmgr.yaml
+```
+
+# Running Docker container of xApp manager
+```sh
+make docker-run
+```
+
+# Deploy, undeploying xApps and querying status (using CURL command)
+```sh
+# Deploy a new xApp instance with the name 'dummy-xapp'
+curl -H "Content-Type: application/json" -X POST http://172.17.0.3:8080/ric/v1/xapps -d '{"name": "dummy-xapp"}'
+```
+
+```sh
+# Query the status of all xApp applications
+curl -H "Content-Type: application/json" http://localhost:8080/ric/v1/xapps
+% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100    95  100    95    0     0  95000      0 --:--:-- --:--:-- --:--:-- 95000
+[
+    {
+        "name": "dummy-xapp",
+        "status": "DEPLOYED",
+        "version": "1.0",
+        "instances": [
+            {
+                "name": "dummy-xapp-8984fc9fd-8jq9q",
+                "status": "Running",
+                "ip": "10.99.213.161",
+                "port": 80,
+                "txMessages": "[]",
+                "rxMessages": "[]"
+            },
+            {
+                "name": "dummy-xapp-8984fc9fd-zq47z",
+                "status": "Running",
+                "ip": "10.99.213.161",
+                "port": 80,
+                "txMessages": "[]",
+                "rxMessages": "[]"
+            },
+            {
+                "name": "dummy-xapp-8984fc9fd-zzxjj",
+                "status": "Running",
+                "ip": "10.99.213.161",
+                "port": 80,
+                "txMessages": "[]",
+                "rxMessages": "[]"
+            }
+        ]
+    }
+]
+```
+```sh
+# Query the status of a sigle xApp (using the xApp name)
+curl -H "Content-Type: application/json"  http://localhost:8080/ric/v1/xapps/dummy-xapp
+% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100    95  100    95    0     0  95000      0 --:--:-- --:--:-- --:--:-- 95000
+{
+    "name": "dummy-xapp",
+    "status": "DEPLOYED",
+    "version": "1.0",
+    "instances": [
+        {
+            "name": "dummy-xapp-8984fc9fd-8jq9q",
+            "status": "Running",
+            "ip": "10.99.213.161",
+            "port": 80,
+            "txMessages": "[]",
+            "rxMessages": "[]"
+        },
+        {
+            "name": "dummy-xapp-8984fc9fd-zq47z",
+            "status": "Running",
+            "ip": "10.99.213.161",
+            "port": 80,
+            "txMessages": "[]",
+            "rxMessages": "[]"
+        },
+        {
+            "name": "dummy-xapp-8984fc9fd-zzxjj",
+            "status": "Running",
+            "ip": "10.99.213.161",
+            "port": 80,
+            "txMessages": "[]",
+            "rxMessages": "[]"
+        }
+    ]
+}
+```
+```sh
+# Query the status of a sigle xApp instance (using the xApp instance name)
+curl -H "Content-Type: application/json"  http://localhost:8080/ric/v1/xapps/dummy-xapp
+% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100    95  100    95    0     0  95000      0 --:--:-- --:--:-- --:--:-- 95000
+{
+    "name": "dummy-xapp-8984fc9fd-8jq9q",
+    "status": "Running",
+    "ip": "10.99.213.161",
+    "port": 80,
+    "txMessages": "[]",
+    "rxMessages": "[]"
+}
+```
+```sh
+# Undeploy xApp by name
+curl -H "Content-Type: application/json"  -X DELETE http://localhost:8080/ric/v1/xapps/dummy-xapp
+```
+
+# Health Check Probes (using CURL command)
+```sh
+# Health Check using CURL
+curl -H "Content-Type: application/json" http://10.244.1.47:8080/ric/v1/health --verbose
+*   Trying 10.244.1.47...
+* TCP_NODELAY set
+* Connected to 10.244.1.47 (10.244.1.47) port 8080 (#0)
+> GET /ric/v1/health HTTP/1.1
+> Host: 10.244.1.47:8080
+> User-Agent: curl/7.58.0
+> Accept: */*
+> Content-Type: application/json
+> 
+< HTTP/1.1 200 OK
+< Content-Type: application/json
+< Date: Sun, 24 Mar 2019 11:13:59 GMT
+< Content-Length: 0
+< 
+* Connection #0 to host 10.244.1.47 left intact
+```
+
+# Subsciptions: List, create, update and delete (using CURL command)
+```sh
+# Add a new subscription
+curl -H "Content-Type: application/json" http://172.17.0.3:8080/ric/v1/subscriptions -X POST -d '{"maxRetries": 3, "retryTimer": 5, "eventType":"Created", "targetUrl": "http://192.168.0.12:8088/"}'
+
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100   169  100    70  100    99  17500  24750 --:--:-- --:--:-- --:--:-- 56333
+{
+    "id": "1ILBltYYzEGzWRrVPZKmuUmhwcc",
+    "version": 0,
+    "eventType": "Created"
+}
+```
+```sh
+# List all subscriptions
+curl -H "Content-Type: application/json" http://172.17.0.3:8080/ric/v1/subscriptions
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100   259  100   259    0     0   252k      0 --:--:-- --:--:-- --:--:--  252k
+[
+    {
+        "id": "1ILBZTtEVVtQmIZnh1OJdBP7bcR",
+        "targetUrl": "http://192.168.0.12:8088/",
+        "eventType": "Created",
+        "maxRetries": 3,
+        "retryTimer": 5
+    },
+    {
+        "id": "1ILBltYYzEGzWRrVPZKmuUmhwcc",
+        "targetUrl": "http://192.168.0.12:8088/",
+        "eventType": "Created",
+        "maxRetries": 3,
+        "retryTimer": 5
+    }
+]
+```
+
+```sh
+# Get a specific subscription by Id
+curl -H "Content-Type: application/json" http://172.17.0.3:8080/ric/v1/subscriptions/1ILBZTtEVVtQmIZnh1OJdBP7bcR
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100   128  100   128    0     0   125k      0 --:--:-- --:--:-- --:--:--  125k
+{
+    "id": "1ILBZTtEVVtQmIZnh1OJdBP7bcR",
+    "targetUrl": "http://192.168.0.12:8088/",
+    "eventType": "Created",
+    "maxRetries": 3,
+    "retryTimer": 5
+}
+```
+
+```sh
+# Delete a specific subscription by Id
+curl -H "Content-Type: application/json" http://172.17.0.3:8080/ric/v1/subscriptions/1ILBZTtEVVtQmIZnh1OJdBP7bcR -X DELETE
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
+```
+
+```sh
+# Example of subscription notification POSTed to targetUrl provided by the client
+
+{
+       "id": "1ILBltYYzEGzWRrVPZKmuUmhwcc",
+       "version": 0,
+       "eventType": "Created",
+       "xapp": {
+               "name": "dummy-xapp",
+               "status": "DEPLOYED",
+               "version": "1.0",
+               "instances": [
+                       {
+                               "name": "dummy-xapp-8984fc9fd-lh7r2",
+                               "status": "ContainerCreating",
+                               "ip": "10.104.73.185",
+                               "port": 80,
+                               "txMessages": "[]",
+                               "rxMessages": "[]"
+                       },
+                       {
+                               "name": "dummy-xapp-8984fc9fd-lzrdk",
+                               "status": "Pending",
+                               "ip": "10.104.73.185",
+                               "port": 80,
+                               "txMessages": "[]",
+                               "rxMessages": "[]"
+                       },
+                       {
+                               "name": "dummy-xapp-8984fc9fd-xfjcn",
+                               "status": "Pending",
+                               "ip": "10.104.73.185",
+                               "port": 80,
+                               "txMessages": "[]",
+                               "rxMessages": "[]"
+                       }
+               ]
+       }
+}
+```
+
+# Using xapp manager CLI (appmgrcli) to manage xapps (deploy, get, undeploy, etc)
+
+Run command *appmgrcli help* for short usage instructions, or read the
+script source; the instructions can be found as plain text near the
+beginning.
+
+Unlike direct curl commands, using the *appmgrcli* validates some of
+the parameters, and there is usually less to type...
+
+The host and port where the xapp manager is running are given by
+options *-h* and *-p*, or you can define environment variables
+APPMGR_HOST and APPMGR_PORT to specify them (recommended). The
+following examples assume they have been specified.
+
+```sh
+# Deploy a xapp
+
+$ appmgrcli deploy dummy-xapp
+{
+    "name": "dummy-xapp",
+    "status": "DEPLOYED",
+    "version": "1.0",
+    "instances": [
+        {
+            "name": "dummy-xapp-667dfc9bfb-wd5m9",
+            "status": "Pending",
+            "ip": "",
+            "port": 0,
+            "txMessages": "",
+            "rxMessages": ""
+        }
+    ]
+}
+
+# Undeploy
+
+$ appmgrcli undeploy dummy-xapp
+dummy-xapp undeployed
+
+# Add some subscriptions
+
+$ appmgrcli subscriptions add https://kukkuu.reset created 500 600
+{
+    "id": "1IoQqEI24sPfLkq8prmMqk6Oz1I",
+    "version": 0,
+    "eventType": "created"
+}
+$ appmgrcli subscriptions add https://facebook.com all 10 4
+{
+    "id": "1IoR85ZwgiNiIn82phUR6qJmBvq",
+    "version": 0,
+    "eventType": "all"
+}
+
+# list and delete (also shows using abbreviations):
+
+
+$ appmgrcli subs list
+[
+    {
+        "id": "1IoQqEI24sPfLkq8prmMqk6Oz1I",
+        "targetUrl": "https://kukkuu.reset",
+        "eventType": "created",
+        "maxRetries": 500,
+        "retryTimer": 600
+    },
+    {
+        "id": "1IoR85ZwgiNiIn82phUR6qJmBvq",
+        "targetUrl": "https://facebook.com",
+        "eventType": "all",
+        "maxRetries": 10,
+        "retryTimer": 4
+    }
+]
+
+$ appmgrcli subs del 1IoR85ZwgiNiIn82phUR6qJmBvq
+Subscription 1IoR85ZwgiNiIn82phUR6qJmBvq deleted
+
+$ appmgrcli subs list
+[
+    {
+        "id": "1IoQqEI24sPfLkq8prmMqk6Oz1I",
+        "targetUrl": "https://kukkuu.reset",
+        "eventType": "created",
+        "maxRetries": 500,
+        "retryTimer": 600
+    }
+]
+
+```
+
+# Additional info
+```sh
+Todo
+```
diff --git a/build/Dockerfile b/build/Dockerfile
new file mode 100755 (executable)
index 0000000..f76e31d
--- /dev/null
@@ -0,0 +1,165 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+#----------------------------------------------------------
+#
+#----------------------------------------------------------
+FROM ubuntu:16.04 as ubuntubase
+
+RUN apt-get update -y && \
+    apt-get install -y wget
+
+#RUN sed -i -e 's,http://archive.ubuntu.com/ubuntu,mirror://mirrors.ubuntu.com/mirrors.txt,' /etc/apt/sources.list
+#RUN sed -i -e 's,http://security.ubuntu.com/ubuntu,mirror://mirrors.ubuntu.com/mirrors.txt,' /etc/apt/sources.list
+#RUN sed -i -e 's,http://archive.ubuntu.com/ubuntu,http://mirrors.nic.funet.fi/ubuntu,' /etc/apt/sources.list
+#RUN sed -i -e 's,http://security.ubuntu.com/ubuntu,http://mirrors.nic.funet.fi/ubuntu,' /etc/apt/sources.list
+
+RUN sed -i -e "s,http://archive.ubuntu.com/ubuntu,$(wget -qO - mirrors.ubuntu.com/mirrors.txt | head -1)," /etc/apt/sources.list
+RUN sed -i -e "s,http://security.ubuntu.com/ubuntu,$(wget -qO - mirrors.ubuntu.com/mirrors.txt | head -1)," /etc/apt/sources.list
+
+#
+# packages
+#
+RUN apt-get update -y && \
+    apt-get upgrade -y && \
+    apt-get install -y \
+    build-essential \
+    apt-utils \
+    cmake \
+    make \
+    autoconf \
+    gawk \
+    libtool \
+    automake \
+    pkg-config \
+    sudo \
+    wget \
+    nano \
+    git \
+    jq
+
+#
+# go
+#
+RUN wget https://dl.google.com/go/go1.12.linux-amd64.tar.gz && \
+       tar -C /usr/local -xvf ./go1.12.linux-amd64.tar.gz
+
+ENV PATH="/usr/local/go/bin:${PATH}"
+
+#
+# rancodev libs
+#
+RUN echo "35.165.179.212 gerrit.oran-osc.org" >> /etc/hosts \
+    && mkdir -p /opt/build \
+    && cd /opt/build && git clone https://gerrit.oran-osc.org/r/log \
+    && cd log/ ; ./autogen.sh ; ./configure ; make ; make install \
+    && ldconfig
+
+
+#----------------------------------------------------------
+#
+#----------------------------------------------------------
+FROM ubuntubase as builder
+
+ARG HELMVERSION
+
+#
+# helm
+#
+RUN wget https://storage.googleapis.com/kubernetes-helm/helm-${HELMVERSION}-linux-amd64.tar.gz \
+    && tar -zxvf helm-${HELMVERSION}-linux-amd64.tar.gz \
+    && cp linux-amd64/helm /usr/bin/helm \
+    && rm -rf helm-${HELMVERSION}-linux-amd64.tar.gz \
+    && rm -rf linux-amd64
+
+
+#
+# xapp_manager codes
+#
+RUN mkdir -p /go/src/appmgr
+ENV GOPATH="/go"
+
+#
+# Speed up things by generating layer with needed go packages
+#
+RUN go get github.com/gorilla/mux \
+    && go get github.com/spf13/viper \
+    && go get github.com/gorilla/mux \
+    && go get github.com/orcaman/concurrent-map \
+    && go get github.com/segmentio/ksuid \
+    && go get gopkg.in/yaml.v2
+
+
+COPY . /go/src/appmgr
+
+
+#
+# build
+#
+RUN make -C /go/src/appmgr/build deps
+
+RUN make -C /go/src/appmgr/build build
+
+
+#----------------------------------------------------------
+#
+#----------------------------------------------------------
+FROM builder as test_unit
+WORKDIR "/go/src/appmgr"
+CMD ["make","-C","build", "unit-test"]
+
+
+#----------------------------------------------------------
+#
+#----------------------------------------------------------
+FROM builder as test_sanity
+WORKDIR "/go/src/appmgr"
+CMD ["jq","-s",".", "rest_api/xapp_manager_rest_api.json"]
+
+#----------------------------------------------------------
+#
+#----------------------------------------------------------
+FROM ubuntu:16.04 as release
+
+RUN apt-get update -y \
+    && apt-get install -y sudo openssl ca-certificates ca-cacert \
+    && apt-get clean
+
+
+#
+# libraries and helm
+#
+COPY --from=builder /usr/local/include/ /usr/local/include/
+COPY --from=builder /usr/local/lib/ /usr/local/lib/
+COPY --from=builder /usr/bin/helm /usr/bin/helm
+
+RUN ldconfig
+
+#
+# xApp
+#
+RUN mkdir -p /opt/xAppManager \
+    && chmod -R 755 /opt/xAppManager
+
+COPY --from=builder /go/src/appmgr/build/appmgr /opt/xAppManager/appmgr
+#COPY --from=builder /go/src/appmgr/config/appmgr.yaml /opt/etc/xAppManager/config-file.yaml
+
+
+COPY build/docker-entrypoint.sh /opt/xAppManager/
+
+WORKDIR /opt/xAppManager
+
+ENTRYPOINT ["/opt/xAppManager/docker-entrypoint.sh"]
diff --git a/build/Makefile b/build/Makefile
new file mode 100755 (executable)
index 0000000..266ac05
--- /dev/null
@@ -0,0 +1,129 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+BUILD_DIR=$(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+
+ROOT_DIR:=$(abspath $(BUILD_DIR)/..)
+
+BUILD_PREFIX?="${USER}-"
+
+XAPP_MGR:=appmgr
+XAPP_MGR_DOCKER:=${BUILD_PREFIX}appmgr
+
+GOSRC := $(abspath $(BUILD_DIR)/../src)
+GOFILES := $(GOSRC)/*.go
+COVEROUT := $(abspath $(BUILD_DIR)/cover.out)
+COVERHTML := $(abspath $(BUILD_DIR)/cover.html)
+
+GOCMD=go
+GOBUILD=$(GOCMD) build -a -installsuffix cgo
+GORUN=$(GOCMD) run -a -installsuffix cgo
+GOCLEAN=$(GOCMD) clean
+GOTEST=$(GOCMD) test -v -coverprofile $(COVEROUT)
+GOGET=$(GOCMD) get
+
+HELMVERSION:=v2.13.0-rc.1
+
+#------------------------------------------------------------------------------
+#
+#-------------------------------------------------------------------- ----------
+.PHONY: FORCE build deps run unit-test test-pkg test clean docker-base-build docker-base-clean docker-build docker-run docker-clean docker-test-build docker-test-run-unittest docker-test-run-sanity docker-test-run docker-test-clean
+.DEFAULT: build
+
+default: build
+
+FORCE:
+
+#------------------------------------------------------------------------------
+#
+#------------------------------------------------------------------------------
+
+XAPP_MGR_DOCKER:=$(shell echo $(XAPP_MGR_DOCKER) | tr '[:upper:]' '[:lower:]')
+
+#XAPP_MGR_DOCKER:=$(subst /,_,${XAPP_MGR_DOCKER})
+
+#------------------------------------------------------------------------------
+#
+#------------------------------------------------------------------------------
+
+$(BUILD_DIR)$(XAPP_MGR): deps ${wildcard $(GOFILES)}
+       GO_ENABLED=0 GOOS=linux $(GOBUILD) -o $(BUILD_DIR)$(XAPP_MGR) $(GOFILES)
+
+build: $(BUILD_DIR)$(XAPP_MGR)
+
+deps: ${wildcard $(GOFILES)}
+       cd $(GOSRC) && $(GOGET)
+
+run: $(BUILD_DIR)$(XAPP_MGR)
+       $(BUILD_DIR)$(XAPP_MGR) -host-addr="localhost:8080" -helm-host="localhost:31807" -helm-chart="./"
+
+unit-test:
+       cd $(GOSRC) && $(GOTEST)
+       go tool cover -html=$(COVEROUT) -o $(COVERHTML)
+
+clean:
+       @echo "  >  Cleaning build cache"
+       @-rm -rf $(XAPP_MGR) 2> /dev/null
+       go clean 2> /dev/null
+
+#------------------------------------------------------------------------------
+#
+#------------------------------------------------------------------------------
+
+DCKR_BUILD_OPTS:=${DCKR_BUILD_OPTS} --network=host --build-arg HELMVERSION=${HELMVERSION}
+
+DCKR_RUN_OPTS:=${DCKR_RUN_OPTS} --rm -i
+DCKR_RUN_OPTS:=${DCKR_RUN_OPTS}$(shell test -t 0 && echo ' -t')
+DCKR_RUN_OPTS:=${DCKR_RUN_OPTS}$(shell test -e /etc/localtime && echo ' -v /etc/localtime:/etc/localtime:ro')
+DCKR_RUN_OPTS:=${DCKR_RUN_OPTS}$(shell test -e /var/run/docker.sock && echo ' -v /var/run/docker.sock:/var/run/docker.sock')
+
+
+#------------------------------------------------------------------------------
+#
+#------------------------------------------------------------------------------
+docker-name:
+       @echo $(XAPP_MGR_DOCKER)
+
+docker-build:
+       docker build --target release ${DCKR_BUILD_OPTS} -t $(XAPP_MGR_DOCKER) -f Dockerfile ../.
+
+docker-run:
+       docker run ${DCKR_RUN_OPTS} -v /opt/ric:/opt/ric -p 8080:8080 $(XAPP_MGR_DOCKER)
+
+docker-clean:
+       docker rmi $(XAPP_MGR_DOCKER)
+
+
+#------------------------------------------------------------------------------
+#
+#------------------------------------------------------------------------------
+
+docker-test-build:
+       docker build --target test_unit ${DCKR_BUILD_OPTS} -t ${XAPP_MGR_DOCKER}-test_unit -f Dockerfile ../.
+       docker build --target test_sanity ${DCKR_BUILD_OPTS} -t ${XAPP_MGR_DOCKER}-test_sanity -f Dockerfile ../.
+
+docker-test-run-unit:
+       docker run ${DCKR_RUN_OPTS} ${XAPP_MGR_DOCKER}-test_unit 
+
+docker-test-run-sanity:
+       docker run ${DCKR_RUN_OPTS} ${XAPP_MGR_DOCKER}-test_sanity
+
+docker-test-run: docker-test-run-sanity docker-test-run-unit
+
+docker-test-clean:
+       docker rmi -f ${XAPP_MGR_DOCKER}-test_unit
+       docker rmi -f ${XAPP_MGR_DOCKER}-test_sanity
+
diff --git a/build/docker-entrypoint.sh b/build/docker-entrypoint.sh
new file mode 100755 (executable)
index 0000000..80e99df
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+cp /opt/ric/config/appmgr.yaml /opt/xAppManager/config-file.yaml
+
+# Copy all certificates from mounted folder to root system
+cp /opt/ric/certificates/* /etc/ssl/certs
+
+# Start services, etc.
+/opt/xAppManager/appmgr -f /opt/xAppManager/config-file.yaml
diff --git a/cli/appmgrcli b/cli/appmgrcli
new file mode 100755 (executable)
index 0000000..e31048a
--- /dev/null
@@ -0,0 +1,363 @@
+#!/bin/sh
+#
+# Copyright (c) 2019 AT&T Intellectual Property.
+# Copyright (c) 2019 Nokia.
+#
+# 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.
+#
+#############################
+# Simple cli for xapp manager
+#
+# In addition to standard shell tools, requires packages "curl" and
+# "yajl-tools" (the second provides json_reformat on Ubuntu; on Red Hat-style
+# distributions install "yajl" instead).
+#
+myname=appmgrcli
+
+usage() {
+  cat <<EOF1
+usage: $myname [-h host] [-p port] [-v] command params...
+- command is one of deploy, undeploy, status, subscriptions, health, help
+- (abbreviations dep, undep, stat, subs, heal allowed)
+- Parameters of the commands that may have parameters:
+-- deploy: name of the xapp to deploy
+-- undeploy: name of the xapp to undeploy
+-- status:
+---- No parameters: Lists information about all deployed xapps
+---- xapp name as parameter: Prints information about the given xapp
+---- xapp name and instance: Lists information about the given instance only
+-- subscriptions is followed by sub-command list, add, delete, or modify
+---(abbreviations del and mod for delete and modify are allowed):
+---- list without parameters lists all subscriptions
+---- list with subscription id prints that subscription
+---- add URL eventType maxRetry retryTimer
+------- URL is the URL to notify
+------- eventType one of created,deleted,all
+------- maxRetry and retryTimer are positive decimal numbers
+---- modify id URL eventType maxRetry retryTimer
+------- id is the subscription id (find out with the list command)
+--------the rest of the parameters are like in add
+---- delete id
+------- id is the subscription id to delete (find out with the list command)
+- Default values for host and port can be set in environment
+- variables APPMGR_HOST and APPMGR_PORT
+- Option -v sets verbose mode.
+EOF1
+}
+
+# Defaults
+
+host=localhost
+port=8080
+verbose=0
+
+# Check for environment override
+if [ "x$APPMGR_HOST" != "x" ]; then
+    host="$APPMGR_HOST"
+fi
+if [ "x$APPMGR_PORT" != "x" ]; then
+    port="$APPMGR_PORT"
+fi
+
+# Proper shell option parsing:
+while getopts  "h:p:v" flag
+do
+  # Curiously, getopts does not guard against an argument-requiring option
+  # eating the next option. It also does not handle the -- convention.
+  # Here is how to fix that.
+  if [ "X$OPTARG" = 'X--' ]; then
+    break # Explicit end of options
+  fi
+  if expr -- "$OPTARG" : '-.*' > /dev/null ; then
+    echo $myname: Option -$flag has no required value, or value begins with -,
+    echo - which is disallowed.
+    usage
+    exit 1
+  fi
+  case $flag in
+  (h) host="$OPTARG"
+      ;;
+  (p) port="$OPTARG"
+      ;;
+  (v) verbose=1
+      ;;
+  (*)
+      echo $myname: Bad option letter or required option argument missing.
+      usage
+      exit 1
+      ;;
+  esac
+done
+# Get rid of the option part
+shift $((OPTIND-1))
+
+if [ $verbose = 1 ]; then
+  echo "host = $host"
+  echo "port = $port"
+fi
+
+# Verify command
+
+case $1 in
+  (deploy|dep)
+    cmd=deploy
+    ;;
+  (undeploy|undep)
+    cmd=undeploy
+    ;;
+  (status|stat)
+    cmd=status
+    ;;
+  (subscriptions|subs)
+    cmd=subscriptions
+    ;;
+  (health|heal)
+    cmd=health
+    ;;
+  (help)
+    usage
+    exit 0
+    ;;
+  (*)
+    if [ "x$1" = "x" ]; then
+     echo "$myname: Missing command"
+    else
+     echo "$myname: Unrecognized command $1"
+    fi
+    usage
+    exit 1
+    ;;
+esac
+
+if [ $verbose = 1 ]; then
+  echo "Command $cmd params=$2"
+fi
+
+errfile=`mktemp /tmp/appmgr_e.XXXXXXXXXX`
+resultfile=`mktemp /tmp/appmgr_r.XXXXXXXXXX`
+# Variable status used for the return value of the whole script.
+status=0
+
+# Helper for command execution:
+# Do a rest call with "curl": $1 = method, $2 = path (without host and port
+# which come from variables), $3 data to POST if needed
+# returns 0 if OK, and any returned data is in $resultfile
+# else 1, and error message from curl is in $errfile, which is printed
+# before returning the 1.
+# Also sets $status to the return value.
+#
+# On curl options: --silent --show-error disables progress bar, but allows
+# error messages. --connect-timeout 20 limits waiting for connection to
+# 20 seconds. In practice connection will succeed almost immediately,
+# or in the case of wrong address not at all.
+#
+rest() {
+  local data
+  if [ "x$3" != "x" ]; then
+    data="--data $3"
+  fi
+  if curl --silent --show-error --connect-timeout 20 --header "Content-Type: application/json" -X $1 -o $resultfile "http://${host}:${port}$2" $data 2> $errfile ;then
+    status=0
+  else
+    cat $errfile
+    status=1
+  fi
+  return $status
+}
+
+remove_temps () {
+  rm -f $errfile $resultfile
+}
+
+# Execute command ($cmd guaranteed to be valid)
+# Assumes the API currently implemented.
+# Functions for each command below (except health which is so simple).
+
+base=/ric/v1
+base_xapps=$base/xapps
+base_health=$base/health
+base_subs=$base/subscriptions
+
+do_deploy() {
+  if [ "x$1" != "x" ]; then
+    if rest POST $base_xapps \{\"name\":\"$1\"\} ; then
+      json_reformat < $resultfile
+    fi
+  else
+    echo Error: expected the name of xapp to deploy
+    status=1
+  fi
+}
+
+do_undeploy() {
+  local urlpath
+
+  urlpath=$base_xapps
+  if [ "x$1" != "x" ]; then
+    urlpath="$urlpath/$1"
+    if rest DELETE $urlpath; then
+      # Currently xapp_manager returns an empty result if
+      # undeploy is succesfull. Don't reformat file if empty.
+      if [ -s $resultfile ]; then
+        json_reformat < $resultfile
+      else
+        echo "$1 undeployed"
+      fi
+    fi
+  else
+    echo Error: expected the name of xapp to undeploy
+    status=1
+  fi
+}
+
+do_status() {
+  local urlpath
+
+  urlpath=$base_xapps
+  if [ "x$1" != "x" ]; then
+    urlpath="$urlpath/$1"
+  fi
+  if [ "x$2" != "x" ]; then
+    urlpath="$urlpath/instances/$2"
+  fi
+  if rest GET $urlpath; then
+    json_reformat < $resultfile
+  fi
+}
+
+# This is a bit more complex. $1 is sub-command: list, add, delete, modify
+
+# Validate the subscription data that follows a subscription add or modify
+# subcommand. $1=URL, $2=eventType, $3=maxRetries, $4=retryTimer
+# URL must look like URL, event type must be one of created deleted all,
+# maxRetries and retryTimer must be non-negative numbers.
+# If errors, sets variable status=1 and prints errors, else leaves
+# status unchanged.
+#
+validate_subscription() {
+   if ! expr "$1" : "^http://.*" \| "$1" : "^https://.*" >/dev/null; then
+     echo "$myname: bad URL $1"
+     status=1
+   fi
+   if ! [ "$2" = created -o "$2" = deleted -o "$2" = all ]; then
+     echo "$myname: unrecognized event $2"
+     status=1
+   fi
+   if ! expr "$3" : "^[0-9][0-9]*$" >/dev/null; then
+     echo "$myname: invalid maximum retries count $3"
+     status=1  
+   fi
+   if ! expr "$4" : "^[0-9][0-9]*$" >/dev/null; then
+     echo "$myname: invalid retry time $4"
+     status=1  
+   fi
+}
+
+do_subscriptions() {
+  local urlpath
+  urlpath=$base_subs
+  case $1 in
+    (list)
+      if [ "x$2" != "x" ]; then
+        urlpath="$urlpath/$2"
+      fi
+      if rest GET $urlpath; then
+        json_reformat < $resultfile
+      else
+        status=1
+      fi
+    ;;
+    (add)
+      validate_subscription "$2" "$3" "$4" "$5"
+      if [ $status = 0 ]; then
+        if rest POST $urlpath \{\"targetUrl\":\"$2\",\"eventType\":\"$3\",\"maxRetries\":$4,\"retryTimer\":$5\} ; then
+          json_reformat < $resultfile
+        else
+          status=1
+       fi
+      fi
+    ;;
+    (delete|del)
+      if [ "x$2" != "x" ]; then
+        urlpath="$urlpath/$2"
+      else
+       echo "$myname: Subscription id required"
+       status=1
+      fi
+      if [ $status = 0 ]; then
+        if rest DELETE $urlpath; then
+          # Currently xapp_manager returns an empty result if
+          # delete is succesfull. Don't reformat file if empty.
+          if [ -s $resultfile ]; then
+            json_reformat < $resultfile
+          else
+            echo "Subscription $2 deleted"
+          fi
+        else
+          status=1
+       fi        
+      fi
+    ;;
+    (modify|mod)
+      if [ "x$2" != "x" ]; then
+        urlpath="$urlpath/$2"
+      else
+       echo "$myname: Subscription id required"
+       status=1
+      fi
+      if [ $status = 0 ]; then
+        validate_subscription "$3" "$4" "$5" "$6"
+        if [ $status = 0 ]; then
+          if rest PUT $urlpath \{\"targetUrl\":\"$3\",\"eventType\":\"$4\",\"maxRetries\":$5,\"retryTimer\":$6\} ; then
+            json_reformat < $resultfile
+          else
+            status=1
+          fi
+        fi
+      fi
+    ;;
+    (*)
+      echo "$myname: unrecognized subscriptions subcommand $1"
+      status=1
+  esac
+}
+
+case $cmd in
+  (deploy)
+    do_deploy "$2"
+    ;;
+  (undeploy)
+    do_undeploy "$2"
+    ;;
+  (status)
+    do_status "$2" "$3"
+    ;;
+  (subscriptions)
+    do_subscriptions "$2" "$3" "$4" "$5" "$6" "$7"
+    ;;
+  (health)
+    if rest GET $base_health ; then
+      echo OK
+    else
+      echo NOT OK
+    fi
+    ;;
+esac
+remove_temps
+exit $status
+
+# An Emacs hack to set the indentation style of this file
+# Local Variables:
+# sh-indentation:2
+# End:
diff --git a/config/appmgr.yaml b/config/appmgr.yaml
new file mode 100755 (executable)
index 0000000..3753f1f
--- /dev/null
@@ -0,0 +1,33 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+"local":
+  "host": ":8080"
+"helm":
+  "host": "192.168.0.12:31807"
+  "repo": "/opt/ric/dummy-xapp-chart"
+  "secrets":
+    "username": "admin"
+    "password": "ric"
+  "helm-username-file": "./helm_repo_username"
+  "helm-password-file": "./helm_repo_password"
+"xapp":
+  "namespace": "ricxapp"
+  "tarDir": "/tmp"
+"db":
+  "host": ":6379"
+  "prot": "tcp"
+  "maxIdle": 80
+  "maxActive": 12000
\ No newline at end of file
diff --git a/config/msg_type.yaml b/config/msg_type.yaml
new file mode 100755 (executable)
index 0000000..c700250
--- /dev/null
@@ -0,0 +1,21 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+txMessages:
+  - RIC_E2_TERMINATION_HC_REQUEST
+  - RIC_E2_MANAGER_HC_REQUEST  
+rxMessages:
+  - RIC_E2_TERMINATION_HC_RESPONSE
+  - RIC_E2_MANAGER_HC_RESPONSE
\ No newline at end of file
diff --git a/helm_chart/appmgr/Chart.yaml b/helm_chart/appmgr/Chart.yaml
new file mode 100755 (executable)
index 0000000..36bb213
--- /dev/null
@@ -0,0 +1,20 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+apiVersion: v1
+appVersion: "1.0"
+description: Helm Chart for xAppManager
+name: appmgr
+version: 0.0.2
diff --git a/helm_chart/appmgr/templates/_helpers.tpl b/helm_chart/appmgr/templates/_helpers.tpl
new file mode 100755 (executable)
index 0000000..9e266be
--- /dev/null
@@ -0,0 +1,47 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "appmgr.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "appmgr.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "appmgr.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/helm_chart/appmgr/templates/appconfig.yaml b/helm_chart/appmgr/templates/appconfig.yaml
new file mode 100755 (executable)
index 0000000..b31e1c5
--- /dev/null
@@ -0,0 +1,24 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-appconfig
+data:
+  {{- with .Values.appconfig }}
+    {{- toYaml . | nindent 2 }}
+  {{- end }}
+
diff --git a/helm_chart/appmgr/templates/appenv.yaml b/helm_chart/appmgr/templates/appenv.yaml
new file mode 100755 (executable)
index 0000000..ccda649
--- /dev/null
@@ -0,0 +1,23 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-appenv
+data:
+  {{- with .Values.appenv }}
+    {{- toYaml . | nindent 2 }}
+  {{- end }}
diff --git a/helm_chart/appmgr/templates/deployment.yaml b/helm_chart/appmgr/templates/deployment.yaml
new file mode 100755 (executable)
index 0000000..0ef46d2
--- /dev/null
@@ -0,0 +1,96 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "appmgr.fullname" . }}
+  labels:
+    app.kubernetes.io/name: {{ include "appmgr.name" . }}
+    helm.sh/chart: {{ include "appmgr.chart" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: {{ include "appmgr.name" . }}
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: {{ include "appmgr.name" . }}
+        app.kubernetes.io/instance: {{ .Release.Name }}
+    spec:
+      containers:
+        - name: {{ .Chart.Name }}
+          image: "{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: {{ .Values.image.containerPort }}
+              protocol: TCP
+          volumeMounts:
+            - name: config-volume
+              mountPath: {{ .Values.appconfigpath }}
+            - name: secret-volume
+              mountPath: {{ .Values.appsecretpath }}
+            - name: cert-volume
+              mountPath: {{ .Values.appcertpath }}
+          envFrom:
+            - configMapRef:
+                name: {{ .Release.Name }}-appenv
+          livenessProbe:
+            httpGet:
+              path: {{ .Values.service.health_check_endpoint }}
+              port: 8080
+            initialDelaySeconds: 5
+            periodSeconds: 15
+          readinessProbe:
+            httpGet:
+              path: {{ .Values.service.health_check_endpoint }}
+              port: 8080
+            initialDelaySeconds: 5
+            periodSeconds: 15
+          restartPolicy: Always
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          securityContext:
+            # ubuntu
+            #runAsUser: 1000
+            #allowPrivilegeEscalation: false
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+    {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+
+      volumes:
+        - name: config-volume
+          configMap:
+            name: {{ .Release.Name }}-appconfig
+        - name: secret-volume
+          secret:
+            secretName: {{ .Values.appsecretobject }}
+        - name: cert-volume
+          configMap:
+            name: {{ .Values.appcertobject }}
diff --git a/helm_chart/appmgr/templates/service.yaml b/helm_chart/appmgr/templates/service.yaml
new file mode 100755 (executable)
index 0000000..b89dc64
--- /dev/null
@@ -0,0 +1,35 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Values.service.name }}
+  labels:
+    app.kubernetes.io/name: {{ include "appmgr.name" . }}
+    helm.sh/chart: {{ include "appmgr.chart" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: {{ .Values.service.port }}
+      nodePort: {{ .Values.service.nodePort }}
+      protocol: TCP
+      name: http
+  selector:
+    app.kubernetes.io/name: {{ include "appmgr.name" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
diff --git a/helm_chart/appmgr/values.yaml b/helm_chart/appmgr/values.yaml
new file mode 100755 (executable)
index 0000000..8d71678
--- /dev/null
@@ -0,0 +1,114 @@
+#   Copyright (c) 2019 AT&T Intellectual Property.
+#   Copyright (c) 2019 Nokia.
+#
+#   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.
+
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+# Modify this section to point to Docker image repository
+image:
+  #repository: "snapshot.docker.ranco-dev-tools.eastus.cloudapp.azure.com:10001"
+  repository: "k8s-cluster-docker-helm-repo:5000"
+  
+  #repositoryCred:
+  #  user: docker
+  #  password: docker
+  pullPolicy: IfNotPresent
+  
+# This section describes xAppManager
+  replicaCount: 1
+
+  # xAppmanager Docker image name and tag
+  name: appmgr
+  tag: 1.0.0 
+
+  #nameOverride: ""
+  #fullnameOverride: ""
+
+  containerPort: 8080
+
+service:
+  type: NodePort 
+  port: 8080
+  nodePort: 30218
+  name: appmgr-service
+  health_check_endpoint: ric/v1/health  
+
+# config
+# Path referred in appmgr for retrieving configuration details
+appconfigpath: /opt/ric/config
+appconfig:
+  # To be present as files under appconfigpath
+  # Use your own environment addresses
+  appmgr.yaml: |
+    "local":
+      # Port on which the appmgr REST services are provided
+      "host": ":8080"
+    "helm":
+      # Remote helm repo URL. UPDATE this as required.
+      "repo": "https://k8s-cluster-docker-helm-repo/helm_charts"
+
+      # Repo name referred within the appmgr
+      "repo-name": "helm-repo"
+
+      # Tiller service details in the cluster. UPDATE this as required.
+      "tiller-service": "tiller-deploy"
+      "tiller-namespace": "kube-system"
+      "tiller-port": "44134"
+
+      # helm username and password files
+      "helm-username-file": "/opt/ric/secret/helm_repo_username"
+      "helm-password-file": "/opt/ric/secret/helm_repo_password"
+    "xapp":
+      #Namespace to install xAPPs
+      "namespace": ricxapp
+
+# To be provided as env variables
+appenv:
+  NAME: appmgr-env
+  #ENV1: "envvalue1"
+  #ENV2: "envvalue2"
+
+# secret
+# Path referred in appmgr for retrieving helm repo secrets
+appsecretpath: /opt/ric/secret
+
+# Secret object with credentials that should be created in K8S cluster. 
+# Parameters in this object are currently referred by appmgr to connect to helm repo and these are:
+# helm_repo_username
+# helm_repo_password
+appsecretobject: appmgr-creds
+
+# certificates
+# Path referred in appmgr for retrieving helm repo client certificates
+appcertpath: /opt/ric/certificates
+
+# configMap object in K8S cluster that holds the client side certificates to connect to helm repo.
+# Currently all certificates mounted by this object are copied to /etc/ssl/certs
+appcertobject: appmgr-certs
+
+resources: {}
+# limits:
+#   cpu: 100m
+#   memory: 128Mi
+# requests:
+#   cpu: 100m
+#   memory: 128Mi
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+
diff --git a/rest_api/xapp_manager_rest_api.json b/rest_api/xapp_manager_rest_api.json
new file mode 100644 (file)
index 0000000..951f199
--- /dev/null
@@ -0,0 +1,545 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "description": "This is a draft API for RIC appmgr",
+    "version": "0.0.10",
+    "title": "RIC appmgr",
+    "license": {
+      "name": "Apache 2.0",
+      "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+    }
+  },
+  "host": "hostname",
+  "basePath": "/ric/v1/xapps",
+  "schemes": [
+    "https",
+    "http"
+  ],
+  "paths": {
+    "/ric/v1/health": {
+      "get": {
+        "summary": "Health check of xApp Manager",
+        "operationId": "getHealth",
+        "responses": {
+          "200": {
+            "description": "Status of xApp Manager is ok"
+          }
+        }
+      }
+    },
+    "/ric/v1/xapps": {
+      "post": {
+        "summary": "Deploy a xapp",
+        "operationId": "deployXapp",
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "parameters": [
+          {
+            "name": "xAppInfo",
+            "in": "body",
+            "description": "xApp information",
+            "schema": {
+              "type": "object",
+              "required": [
+                "xAppName"
+              ],
+              "properties": {
+                "xAppName": {
+                  "type":"string",
+                  "description":"Name of the xApp",
+                  "example": "xapp-dummy"
+                }
+              }
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "xApp successfully created",
+            "schema": {
+              "$ref": "#/definitions/Xapp"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          },
+          "500": {
+            "description": "Internal error"
+          }
+        }
+      },
+      "get": {
+        "summary": "Returns the status of all xapps",
+        "operationId": "getAllXapps",
+        "produces": [
+          "application/json"
+        ],
+        "responses": {
+          "200": {
+            "description": "successful query of xApps",
+            "schema": {
+              "$ref": "#/definitions/AllXapps"
+            }
+          },
+          "500": {
+            "description": "Internal error"
+          }
+        }
+      }
+    },
+    "/ric/v1/xapps/{xAppName}": {
+      "get": {
+        "summary": "Returns the status of a given xapp",
+        "operationId": "getXappByName",
+        "produces": [
+          "application/json"
+        ],
+        "parameters": [
+          {
+            "name": "xAppName",
+            "in": "path",
+            "description": "Name of xApp",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "schema": {
+              "$ref": "#/definitions/Xapp"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Xapp not found"
+          },
+          "500": {
+            "description": "Internal error"
+          }
+        }
+      },
+      "delete": {
+        "summary": "Undeploy an existing xapp",
+        "operationId": "undeployXapp",
+        "parameters": [
+          {
+            "name": "xAppName",
+            "in": "path",
+            "description": "Xapp to be undeployed",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Successful deletion of xApp"
+          },
+          "400": {
+            "description": "Invalid xApp name supplied"
+          },
+          "500": {
+            "description": "Internal error"
+          }
+        }
+      }
+    },
+    "/ric/v1/xapps/{xAppName}/instances/{xAppInstanceName}": {
+      "get": {
+        "summary": "Returns the status of a given xapp",
+        "operationId": "getXappInstanceByName",
+        "produces": [
+          "application/json"
+        ],
+        "parameters": [
+          {
+            "name": "xAppName",
+            "in": "path",
+            "description": "Name of xApp",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "xAppInstanceName",
+            "in": "path",
+            "description": "Name of xApp instance to get information",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "schema": {
+              "$ref": "#/definitions/XappInstance"
+            }
+          },
+          "400": {
+            "description": "Invalid name supplied"
+          },
+          "404": {
+            "description": "Xapp not found"
+          },
+          "500": {
+            "description": "Internal error"
+          }
+        }
+      }
+    },
+    "/ric/v1/subscriptions": {
+      "post": {
+        "summary": "Subscribe event",
+        "operationId": "addSubscription",
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "parameters": [
+          {
+            "name": "subscriptionRequest",
+            "in": "body",
+            "description": "New subscription",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/subscriptionRequest"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Subscription successful",
+            "schema": {
+              "$ref": "#/definitions/subscriptionResponse"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "get": {
+        "summary": "Returns all subscriptions",
+        "operationId": "getSubscriptions",
+        "produces": [
+          "application/json"
+        ],
+        "responses": {
+          "200": {
+            "description": "successful query of subscriptions",
+            "schema": {
+              "$ref": "#/definitions/allSubscriptions"
+            }
+          }
+        }
+      }
+    },
+    "/ric/v1/subscriptions/{subscriptionId}": {
+      "get": {
+        "summary": "Returns the information of subscription",
+        "operationId": "getSubscriptionById",
+        "produces": [
+          "application/json"
+        ],
+        "parameters": [
+          {
+            "name": "subscriptionId",
+            "in": "path",
+            "description": "ID of subscription",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "schema": {
+              "$ref": "#/definitions/subscription"
+            }
+          },
+          "400": {
+            "description": "Invalid ID supplied"
+          },
+          "404": {
+            "description": "Subscription not found"
+          }
+        }
+      },
+      "put": {
+        "summary": "Modify event subscription",
+        "operationId": "modifySubscription",
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "parameters": [
+          {
+            "name": "subscriptionId",
+            "in": "path",
+            "description": "ID of subscription",
+            "required": true,
+            "type": "integer"
+          },
+          {
+            "in": "body",
+            "name": "subscriptionRequest",
+            "description": "Modified subscription",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/subscriptionRequest"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Subscription modification successful",
+            "schema": {
+              "$ref": "#/definitions/subscriptionResponse"
+            }
+          },
+          "400": {
+            "description": "Invalid input"
+          }
+        }
+      },
+      "delete": {
+        "summary": "Unsubscribe event",
+        "description": "",
+        "operationId": "deleteSubscription",
+        "parameters": [
+          {
+            "name": "subscriptionId",
+            "in": "path",
+            "description": "ID of subscription",
+            "required": true,
+            "type": "integer"
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Successful deletion of subscription"
+          },
+          "400": {
+            "description": "Invalid subscription supplied"
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "AllXapps": {
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Xapp"
+      }
+    },
+    "Xapp": {
+      "type": "object",
+      "required": [
+        "name"
+      ],
+      "properties": {
+        "name": {
+          "type": "string",
+          "example": "xapp-dummy"
+        },
+        "status": {
+          "type": "string",
+          "description": "xapp status in the RIC",
+          "enum": [
+            "unknown",
+            "deployed",
+            "deleted",
+            "superseded",
+            "failed",
+            "deleting"
+          ]
+        },
+        "version": {
+          "type": "string",
+          "example": "1.2.3"
+        },
+        "instances": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/XappInstance"
+          }
+        }
+      }
+    },
+    "XappInstance": {
+      "type": "object",
+      "required": [
+        "name"
+      ],
+      "properties": {
+        "name": {
+          "type": "string",
+          "example": "xapp-dummy-6cd577d9-4v255"
+        },
+        "status": {
+          "type": "string",
+          "description": "xapp instance status",
+          "enum": [
+            "pending",
+            "running",
+            "succeeded",
+            "failed",
+            "unknown",
+            "completed",
+            "crashLoopBackOff"
+          ]
+        },
+        "ip": {
+          "type": "string",
+          "example": "192.168.0.1"
+        },
+        "port": {
+          "type": "integer",
+          "example": 32300
+        },
+        "txMessages" : {
+          "type": "array",
+          "items": {
+            "type" : "string",
+            "example" : "ControlIndication"
+          }
+        },
+        "rxMessages" : {
+          "type": "array",
+          "items": {
+            "type" : "string",
+            "example" : "LoadIndication"
+          }
+        }
+      }
+    },
+    "subscriptionRequest": {
+      "type": "object",
+      "required": [
+        "targetUrl",
+        "eventType",
+        "maxRetries",
+        "retryTimer"
+      ],
+      "properties": {
+        "targetUrl": {
+          "type": "string",
+          "example": "http://localhost:11111/apps/webhook/"
+        },
+        "eventType": {
+          "type": "string",
+          "description": "Event which is subscribed",
+          "enum": [
+            "created",
+            "deleted",
+            "all"
+          ]
+        },
+        "maxRetries": {
+          "type": "integer",
+          "description": "Maximum number of retries",
+          "example": 11
+        },
+        "retryTimer": {
+          "type": "integer",
+          "description": "Time in seconds to wait before next retry",
+          "example": 22
+        }
+      }
+    },
+    "subscriptionResponse": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "example": "1ILBltYYzEGzWRrVPZKmuUmhwcc"
+        },
+        "version": {
+          "type": "integer",
+          "example": 2
+        },
+        "eventType": {
+          "type": "string",
+          "description": "Event which is subscribed",
+          "enum": [
+            "created",
+            "deleted",
+            "all"
+          ]
+        }
+      }
+    },
+    "allSubscriptions": {
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/subscription"
+      }
+    },
+    "subscription": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "example": "1ILBltYYzEGzWRrVPZKmuUmhwcc"
+        },
+        "targetUrl": {
+          "type": "string",
+          "example": "http://localhost:11111/apps/webhook/"
+        },
+        "eventType": {
+          "type": "string",
+          "description": "Event which is subscribed",
+          "enum": [
+            "created",
+            "deleted",
+            "all"
+          ]
+        },
+        "maxRetries": {
+          "type": "integer",
+          "description": "Maximum number of retries",
+          "example": 11
+        },
+        "retryTimer": {
+          "type": "integer",
+          "description": "Time in seconds to wait before next retry",
+          "example": 22
+        }
+      }
+    },
+    "subscriptionNotification": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "example": "1ILBltYYzEGzWRrVPZKmuUmhwcc"
+        },
+        "version": {
+          "type": "integer",
+          "example": 2
+        },
+        "eventType": {
+          "type": "string",
+          "description": "Event to be notified",
+          "enum": [
+            "created",
+            "deleted"
+          ]
+        },
+        "xApps": {
+          "$ref": "#/definitions/AllXapps"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/api.go b/src/api.go
new file mode 100755 (executable)
index 0000000..b9c2b63
--- /dev/null
@@ -0,0 +1,251 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "encoding/json"
+    "github.com/gorilla/mux"
+    "github.com/spf13/viper"
+    "log"
+    "net/http"
+)
+
+// API functions
+
+func (m *XappManager) Initialize(h Helmer) *mux.Router {
+    m.sd = SubscriptionDispatcher{}
+    m.sd.Initialize()
+    m.helm = h
+    m.router = mux.NewRouter().StrictSlash(true)
+
+    resources := []Resource{
+        Resource{"GET", "/ric/v1/health", m.getHealthStatus},
+
+        Resource{"GET", "/ric/v1/xapps", m.getAllXapps},
+        Resource{"GET", "/ric/v1/xapps/{name}", m.getXappByName},
+        Resource{"GET", "/ric/v1/xapps/{name}/instances/{id}", m.getXappInstanceByName},
+        Resource{"POST", "/ric/v1/xapps", m.deployXapp},
+        Resource{"DELETE", "/ric/v1/xapps/{name}", m.undeployXapp},
+
+        Resource{"GET", "/ric/v1/subscriptions", m.getSubscriptions},
+        Resource{"POST", "/ric/v1/subscriptions", m.addSubscription},
+        Resource{"GET", "/ric/v1/subscriptions/{id}", m.getSubscription},
+        Resource{"DELETE", "/ric/v1/subscriptions/{id}", m.deleteSubscription},
+        Resource{"PUT", "/ric/v1/subscriptions/{id}", m.updateSubscription},
+    }
+
+    for _, resource := range resources {
+        handler := Logger(resource.HandlerFunc)
+        m.router.Methods(resource.Method).Path(resource.Url).Handler(handler)
+    }
+
+    return m.router
+}
+
+// Health monitoring
+func (m *XappManager) getHealthStatus(w http.ResponseWriter, r *http.Request) {
+    respondWithJSON(w, http.StatusOK, nil)
+}
+
+// API: XAPP handlers
+func (m *XappManager) Run() {
+    host := viper.GetString("local.host")
+    if host == "" {
+        host = ":8080"
+    }
+    log.Printf("Xapp manager started ... serving on %s\n", host)
+
+    log.Fatal(http.ListenAndServe(host, m.router))
+}
+
+func (m *XappManager) getXappByName(w http.ResponseWriter, r *http.Request) {
+    xappName, ok := getResourceId(r, w, "name")
+    if ok != true {
+        return
+    }
+
+    if xapp, err := m.helm.Status(xappName); err == nil {
+        respondWithJSON(w, http.StatusOK, xapp)
+    } else {
+        respondWithError(w, http.StatusNotFound, err.Error())
+    }
+}
+
+func (m *XappManager) getXappInstanceByName(w http.ResponseWriter, r *http.Request) {
+    xappName, ok := getResourceId(r, w, "name")
+    if ok != true {
+        return
+    }
+
+    xapp, err := m.helm.Status(xappName)
+    if err != nil {
+        respondWithError(w, http.StatusNotFound, err.Error())
+        return
+    }
+
+    xappInstanceName, ok := getResourceId(r, w, "id")
+    if ok != true {
+        return
+    }
+
+    for _, v := range xapp.Instances {
+        if v.Name == xappInstanceName {
+            respondWithJSON(w, http.StatusOK, v)
+            return
+        }
+    }
+    mdclog(Mdclog_err, "Xapp instance not found - url=" + r.URL.RequestURI())
+
+    respondWithError(w, http.StatusNotFound, "Xapp instance not found")
+}
+
+func (m *XappManager) getAllXapps(w http.ResponseWriter, r *http.Request) {
+    xapps, err := m.helm.StatusAll()
+    if err != nil {
+        respondWithError(w, http.StatusInternalServerError, err.Error())
+        return
+    }
+
+    respondWithJSON(w, http.StatusOK, xapps)
+}
+
+func (m *XappManager) deployXapp(w http.ResponseWriter, r *http.Request) {
+    if r.Body == nil {
+        mdclog(Mdclog_err, "No xapp data found in request body - url=" + r.URL.RequestURI())
+        respondWithError(w, http.StatusMethodNotAllowed, "No xapp data!")
+        return
+    }
+
+    var xapp Xapp
+    if err := json.NewDecoder(r.Body).Decode(&xapp); err != nil {
+        mdclog(Mdclog_err, "Invalid xapp data in request body - url=" + r.URL.RequestURI())
+        respondWithError(w, http.StatusMethodNotAllowed, "Invalid xapp data!")
+        return
+    }
+    defer r.Body.Close()
+
+    xapp, err := m.helm.Install(xapp.Name)
+    if err != nil {
+        respondWithError(w, http.StatusInternalServerError, err.Error())
+        return
+    }
+
+    respondWithJSON(w, http.StatusCreated, xapp)
+
+    m.sd.Publish(xapp, EventType("created"))
+}
+
+func (m *XappManager) undeployXapp(w http.ResponseWriter, r *http.Request) {
+    xappName, ok := getResourceId(r, w, "name")
+    if ok != true {
+        return
+    }
+
+    xapp, err := m.helm.Delete(xappName)
+    if err != nil {
+        respondWithError(w, http.StatusInternalServerError, err.Error())
+        return
+    }
+
+    respondWithJSON(w, http.StatusNoContent, nil)
+
+    m.sd.Publish(xapp, EventType("deleted"))
+}
+
+// API: resthook handlers
+func (m *XappManager) getSubscriptions(w http.ResponseWriter, r *http.Request) {
+    respondWithJSON(w, http.StatusOK, m.sd.GetAll())
+}
+
+func (m *XappManager) getSubscription(w http.ResponseWriter, r *http.Request) {
+    if id, ok := getResourceId(r, w, "id"); ok == true {
+        if s, ok := m.sd.Get(id); ok {
+            respondWithJSON(w, http.StatusOK, s)
+        } else {
+            mdclog(Mdclog_err, "Subscription not found - url=" + r.URL.RequestURI())
+            respondWithError(w, http.StatusNotFound, "Subscription not found")
+        }
+    }
+}
+
+func (m *XappManager) deleteSubscription(w http.ResponseWriter, r *http.Request) {
+    if id, ok := getResourceId(r, w, "id"); ok == true {
+        if _, ok := m.sd.Delete(id); ok {
+            respondWithJSON(w, http.StatusNoContent, nil)
+        } else {
+            mdclog(Mdclog_err, "Subscription not found - url=" + r.URL.RequestURI())
+            respondWithError(w, http.StatusNotFound, "Subscription not found")
+        }
+    }
+}
+
+func (m *XappManager) addSubscription(w http.ResponseWriter, r *http.Request) {
+    var req SubscriptionReq
+    if r.Body == nil || json.NewDecoder(r.Body).Decode(&req) != nil {
+        mdclog(Mdclog_err, "Invalid request payload - url=" + r.URL.RequestURI())
+        respondWithError(w, http.StatusMethodNotAllowed, "Invalid request payload")
+        return
+    }
+    defer r.Body.Close()
+
+    respondWithJSON(w, http.StatusCreated, m.sd.Add(req))
+}
+
+func (m *XappManager) updateSubscription(w http.ResponseWriter, r *http.Request) {
+    if id, ok := getResourceId(r, w, "id"); ok == true {
+        var req SubscriptionReq
+        if r.Body == nil || json.NewDecoder(r.Body).Decode(&req) != nil {
+            mdclog(Mdclog_err, "Invalid request payload - url=" + r.URL.RequestURI())
+            respondWithError(w, http.StatusMethodNotAllowed, "Invalid request payload")
+            return
+        }
+        defer r.Body.Close()
+
+        if s, ok := m.sd.Update(id, req); ok {
+            respondWithJSON(w, http.StatusOK, s)
+        } else {
+            mdclog(Mdclog_err, "Subscription not found - url=" + r.URL.RequestURI())
+            respondWithError(w, http.StatusNotFound, "Subscription not found")
+        }
+    }
+}
+
+// Helper functions
+func respondWithError(w http.ResponseWriter, code int, message string) {
+    respondWithJSON(w, code, map[string]string{"error": message})
+}
+
+func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
+    w.Header().Set("Content-Type", "application/json")
+    w.WriteHeader(code)
+    if payload != nil {
+        response, _ := json.Marshal(payload)
+        w.Write(response)
+    }
+}
+
+func getResourceId(r *http.Request, w http.ResponseWriter, pattern string) (id string, ok bool) {
+    if id, ok = mux.Vars(r)[pattern]; ok != true {
+        mdclog(Mdclog_err, "Couldn't resolve name/id from the request URL")
+        respondWithError(w, http.StatusMethodNotAllowed, "Couldn't resolve name/id from the request URL")
+        return
+    }
+    return
+}
diff --git a/src/api_test.go b/src/api_test.go
new file mode 100755 (executable)
index 0000000..7610f49
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "net/http"
+    "net/http/httptest"
+    "testing"
+    "reflect"
+    "strconv"
+    "os"
+    "bytes"
+    "errors"
+    "encoding/json"
+    "github.com/gorilla/mux"
+)
+
+var x XappManager
+var xapp Xapp
+var xapps []Xapp
+var helmError error
+
+type MockedHelmer struct {
+}
+
+func (h *MockedHelmer) Status(name string) (Xapp, error) {
+    return xapp, helmError
+}
+
+func (h *MockedHelmer) StatusAll() ([]Xapp, error) {
+    return xapps, helmError
+}
+
+func (h *MockedHelmer) List() (names []string, err error) {
+    return names, helmError
+}
+
+func (h *MockedHelmer) Install(name string) (Xapp, error) {
+    return xapp, helmError
+}
+
+func (h *MockedHelmer) Delete(name string) (Xapp, error) {
+    return xapp, helmError
+}
+
+// Test cases
+func TestMain(m *testing.M) {
+    loadConfig();
+
+    xapp = Xapp{}
+    xapps = []Xapp{}
+
+    h := MockedHelmer{}
+    x = XappManager{}
+    x.Initialize(&h)
+
+    // Just run on the background (for coverage)
+    go x.Run()
+
+    code := m.Run()
+    os.Exit(code)
+}
+
+func TestGetHealthCheck(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/health", nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusOK, response.Code)
+}
+
+func TestGetAppsReturnsEmpty(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps", nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusOK, response.Code)
+    if body := response.Body.String(); body != "[]" {
+        t.Errorf("handler returned unexpected body: got %v want []", body)
+    }
+}
+
+func TestCreateXApp(t *testing.T) {
+    xapp = generateXapp("dummy-xapp", "started", "1.0", "dummy-xapp-1234-5678", "running", "127.0.0.1", "9999")
+
+    payload := []byte(`{"name":"dummy-xapp"}`)
+    req, _ := http.NewRequest("POST", "/ric/v1/xapps", bytes.NewBuffer(payload))
+    response := executeRequest(req)
+
+    checkResponseData(t, response, http.StatusCreated, false)
+}
+
+func TestGetAppsReturnsListOfXapps(t *testing.T) {
+    xapps = append(xapps, xapp)
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps", nil)
+    response := executeRequest(req)
+
+    checkResponseData(t, response, http.StatusOK, true)
+}
+
+func TestGetAppByIdReturnsGivenXapp(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps/" + xapp.Name, nil)
+    response := executeRequest(req)
+
+    checkResponseData(t, response, http.StatusOK, false)
+}
+
+func TestGetAppInstanceByIdReturnsGivenXapp(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps/" + xapp.Name + "/instances/dummy-xapp-1234-5678", nil)
+    response := executeRequest(req)
+
+    var ins XappInstance
+    checkResponseCode(t, http.StatusOK, response.Code)
+    json.NewDecoder(response.Body).Decode(&ins)
+
+    if !reflect.DeepEqual(ins, xapp.Instances[0]) {
+        t.Errorf("handler returned unexpected body: got: %v, expected: %v", ins, xapp.Instances[0])
+    }
+}
+
+func TestDeleteAppRemovesGivenXapp(t *testing.T) {
+    req, _ := http.NewRequest("DELETE", "/ric/v1/xapps/" + xapp.Name, nil)
+    response := executeRequest(req)
+
+    checkResponseData(t, response, http.StatusNoContent, false)
+
+    // Xapp not found from the Redis DB
+    helmError = errors.New("Not found")
+
+    req, _ = http.NewRequest("GET", "/ric/v1/xapps/" + xapp.Name, nil)
+    response = executeRequest(req)
+    checkResponseCode(t, http.StatusNotFound, response.Code)
+}
+
+// Error handling
+func TestGetXappReturnsError(t *testing.T) {
+    helmError = errors.New("Not found")
+
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps/invalidXappName", nil)
+    response := executeRequest(req)
+    checkResponseCode(t, http.StatusNotFound, response.Code)
+}
+
+func TestGetXappInstanceReturnsError(t *testing.T) {
+    helmError = errors.New("Some error")
+
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps/" + xapp.Name + "/instances/invalidXappName", nil)
+    response := executeRequest(req)
+    checkResponseCode(t, http.StatusNotFound, response.Code)
+}
+
+func TestGetXappListReturnsError(t *testing.T) {
+    helmError = errors.New("Internal error")
+
+    req, _ := http.NewRequest("GET", "/ric/v1/xapps", nil)
+    response := executeRequest(req)
+    checkResponseCode(t, http.StatusInternalServerError, response.Code)
+}
+
+func TestCreateXAppWithoutXappData(t *testing.T) {
+    req, _ := http.NewRequest("POST", "/ric/v1/xapps", nil)
+    response := executeRequest(req)
+    checkResponseData(t, response, http.StatusMethodNotAllowed, false)
+}
+
+func TestCreateXAppWithInvalidXappData(t *testing.T) {
+    body := []byte("Invalid JSON data ...")
+
+    req, _ := http.NewRequest("POST", "/ric/v1/xapps", bytes.NewBuffer(body))
+    response := executeRequest(req)
+    checkResponseData(t, response, http.StatusMethodNotAllowed, false)
+}
+
+func TestCreateXAppReturnsError(t *testing.T) {
+    helmError = errors.New("Not found")
+
+    payload := []byte(`{"name":"dummy-xapp"}`)
+    req, _ := http.NewRequest("POST", "/ric/v1/xapps", bytes.NewBuffer(payload))
+    response := executeRequest(req)
+
+    checkResponseData(t, response, http.StatusInternalServerError, false)
+}
+
+func TestDeleteXappListReturnsError(t *testing.T) {
+    helmError = errors.New("Internal error")
+
+    req, _ := http.NewRequest("DELETE", "/ric/v1/xapps/invalidXappName", nil)
+    response := executeRequest(req)
+    checkResponseCode(t, http.StatusInternalServerError, response.Code)
+}
+
+// Helper functions
+type fn func(w http.ResponseWriter, r *http.Request)
+
+func executeRequest(req *http.Request) *httptest.ResponseRecorder {
+    rr := httptest.NewRecorder()
+
+    vars := map[string]string{
+        "id": "1",
+    }
+    req = mux.SetURLVars(req, vars)
+
+    x.router.ServeHTTP(rr, req)
+
+    return rr
+}
+
+func checkResponseCode(t *testing.T, expected, actual int) {
+    if expected != actual {
+        t.Errorf("Expected response code %d. Got %d\n", expected, actual)
+    }
+}
+
+func checkResponseData(t *testing.T, response *httptest.ResponseRecorder, expectedHttpStatus int, isList bool) {
+    expectedData := xapp
+
+    checkResponseCode(t, expectedHttpStatus, response.Code)
+    if isList == true {
+        jsonResp := []Xapp{}
+        json.NewDecoder(response.Body).Decode(&jsonResp)
+
+        if !reflect.DeepEqual(jsonResp[0], expectedData) {
+            t.Errorf("handler returned unexpected body: %v", jsonResp)
+        }
+    } else {
+        json.NewDecoder(response.Body).Decode(&xapp)
+
+        if !reflect.DeepEqual(xapp, expectedData) {
+            t.Errorf("handler returned unexpected body: got: %v, expected: %v", xapp, expectedData)
+        }
+    }
+}
+
+func generateXapp(name, status, ver, iname, istatus, ip, port string) (x Xapp) {
+    x.Name = name
+    x.Status = status
+    x.Version = ver
+    p , _ := strconv.Atoi(port)
+    instance := XappInstance{
+        Name: iname,
+        Status: istatus,
+        Ip: ip,
+        Port: p,
+        TxMessages: []string{"RIC_E2_TERMINATION_HC_REQUEST", "RIC_E2_MANAGER_HC_REQUEST"},
+        RxMessages: []string{"RIC_E2_TERMINATION_HC_RESPONSE", "RIC_E2_MANAGER_HC_RESPONSE"},
+    }
+    x.Instances = append(x.Instances, instance)
+
+    return
+}
diff --git a/src/config.go b/src/config.go
new file mode 100755 (executable)
index 0000000..e9ede6b
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "flag"
+    "log"
+    "github.com/spf13/viper"
+    "github.com/fsnotify/fsnotify"
+)
+
+const DEFAULT_CONFIG_FILE = "../config/appmgr.yaml"
+
+func parseCmd() string {
+    var fileName *string
+    fileName = flag.String("f", DEFAULT_CONFIG_FILE, "Specify the configuration file.")
+    flag.Parse()
+
+    return *fileName
+}
+
+func loadConfig() {
+    viper.SetConfigFile(parseCmd())
+
+    if err := viper.ReadInConfig(); err != nil {
+        log.Fatalf("Error reading config file, %s", err)
+    }
+    log.Printf("Using config file: %s\n", viper.ConfigFileUsed())
+
+    // Watch for config file changes and re-read data ...
+    watch()
+}
+
+func watch() {
+    viper.WatchConfig()
+    viper.OnConfigChange(func(e fsnotify.Event) {
+        log.Println("config file changed ", e.Name)
+    })
+}
diff --git a/src/helm.go b/src/helm.go
new file mode 100755 (executable)
index 0000000..cfa1f18
--- /dev/null
@@ -0,0 +1,359 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "fmt"
+    "log"
+    "os"
+    "os/exec"
+    "regexp"
+    "strconv"
+    "strings"
+    "github.com/spf13/viper"
+    "gopkg.in/yaml.v2"
+    "io/ioutil"
+    "path"
+)
+
+var execCommand = exec.Command
+
+func Exec(args string) (out []byte, err error) {
+    cmd := execCommand("/bin/sh", "-c", strings.Join([]string{"helm", args}, " "))
+
+    // In testing environment, don't print command traces ...
+    if !strings.HasSuffix(os.Args[0], ".test") {
+        log.Printf("Running command: %v", cmd)
+    }
+
+    out, err = cmd.CombinedOutput()
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("Command failed", args, err.Error()))
+        return out, err
+    }
+
+    if !strings.HasSuffix(os.Args[0], ".test") {
+        mdclog(Mdclog_debug, formatLog("command success", string(out), ""))
+    }
+
+    return out, err
+}
+
+func (h *Helm) Run(args string) (out []byte, err error) {
+    if h.initDone == false {
+        if _, err := h.Init(); err != nil {
+            mdclog(Mdclog_err, formatLog("helm init failed", args, err.Error()))
+            return out, err
+        }
+        mdclog(Mdclog_debug, formatLog("Helm init done successfully!", args, ""))
+
+        // Add helm repo
+        if _, err := h.AddRepo(); err != nil {
+            mdclog(Mdclog_err, formatLog("Helm repo addition failed", args, err.Error()))
+            return out, err
+        }
+
+        mdclog(Mdclog_debug, formatLog("Helm repo added successfully", string(out), ""))
+        h.initDone = true
+    }
+
+    return Exec(args)
+}
+
+// API functions
+func (h *Helm) Init() (out []byte, err error) {
+
+    // Add Tiller address as environment variable
+    if err := addTillerEnv(); err != nil {
+        return out, err
+    }
+
+    return Exec(strings.Join([]string{"init -c"}, ""))
+}
+
+func (h *Helm) AddRepo() (out []byte, err error) {
+
+    // Get helm repo user name and password from files mounted by secret object
+    credFile, err := ioutil.ReadFile(viper.GetString("helm.helm-username-file"))
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("helm_repo_username ReadFile failed", "", err.Error()))
+        return
+    }
+
+    username := " --username " + string(credFile)
+
+    credFile, err = ioutil.ReadFile(viper.GetString("helm.helm-password-file"))
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("helm_repo_password ReadFile failed", "", err.Error()))
+        return
+    }
+
+    pwd := " --password " + string(credFile)
+
+    // Get internal helm repo name
+    rname := viper.GetString("helm.repo-name")
+
+    // Get helm repo address from values.yaml
+    repo := viper.GetString("helm.repo")
+
+    return Exec(strings.Join([]string{"repo add ", rname, " ", repo, username, pwd}, ""))
+}
+
+func (h *Helm) Install(name string) (xapp Xapp, err error) {
+    out, err := h.Run(strings.Join([]string{"repo update "}, ""))
+    if err != nil {
+        return
+    }
+
+    rname := viper.GetString("helm.repo-name")
+
+    ns := getNamespaceArgs()
+    out, err = h.Run(strings.Join([]string{"install ", rname, "/", name, " --name ", name, ns}, ""))
+    if err != nil {
+        return
+    }
+
+    return h.ParseStatus(name, string(out))
+}
+
+func (h *Helm) Status(name string) (xapp Xapp, err error) {
+
+    out, err := h.Run(strings.Join([]string{"status ", name}, ""))
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("Getting xapps status", "", err.Error()))
+        return
+    }
+
+    return h.ParseStatus(name, string(out))
+}
+
+func (h *Helm) StatusAll() (xapps []Xapp, err error) {
+    xappNameList, err := h.List()
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("Helm list failed", "", err.Error()))
+        return
+    }
+
+    return h.parseAllStatus(xappNameList)
+}
+
+func (h *Helm) List() (names []string, err error) {
+
+    ns := getNamespaceArgs()
+    out, err := h.Run(strings.Join([]string{"list --all --output yaml ", ns}, ""))
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("Listing deployed xapps failed", "", err.Error()))
+        return
+    }
+
+    return h.GetNames(string(out))
+}
+
+func (h *Helm) Delete(name string) (xapp Xapp, err error) {
+    xapp, err = h.Status(name)
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("Fetching xapp status failed", "", err.Error()))
+        return
+    }
+
+    _, err = h.Run(strings.Join([]string{"del --purge ", name}, ""))
+    return xapp, err
+}
+
+func (h *Helm) Fetch(name , tarDir string) (error) {
+    if strings.HasSuffix(os.Args[0], ".test") {
+        return nil
+    }
+
+    rname := viper.GetString("helm.repo-name") + "/"
+
+    _, err := h.Run(strings.Join([]string{"fetch --untar --untardir ", tarDir, " ", rname, name}, ""))
+    return err
+}
+
+// Helper functions
+func (h *Helm) GetMessages(name string) (msgs MessageTypes, err error) {
+    tarDir := viper.GetString("xapp.tarDir")
+    if tarDir == "" {
+        tarDir = "/tmp"
+    }
+
+    if h.Fetch(name, tarDir); err != nil {
+        mdclog(Mdclog_err, formatLog("Fetch chart failed", "", err.Error()))
+        return
+    }
+
+    return h.ParseMessages(name, tarDir, "msg_type.yaml")
+
+}
+
+func (h *Helm) ParseMessages(name string, chartDir, msgFile string) (msgs MessageTypes, err error) {
+    yamlFile, err := ioutil.ReadFile(path.Join(chartDir, name, msgFile))
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("ReadFile failed", "", err.Error()))
+        return
+    }
+
+    err = yaml.Unmarshal(yamlFile, &msgs)
+    if err != nil {
+        mdclog(Mdclog_err, formatLog("Unmarshal failed", "", err.Error()))
+        return
+    }
+
+    if err = os.RemoveAll(path.Join(chartDir, name)); err != nil {
+        mdclog(Mdclog_err, formatLog("RemoveAll failed", "", err.Error()))
+    }
+
+    return
+}
+
+func (h *Helm) GetVersion(name string) (version string) {
+
+    ns := getNamespaceArgs()
+    out, err := h.Run(strings.Join([]string{"list --output yaml ", name, ns}, ""))
+    if err != nil {
+        return
+    }
+
+    var re = regexp.MustCompile(`AppVersion: .*`)
+    ver := re.FindStringSubmatch(string(out))
+    if ver != nil {
+        version = strings.Split(ver[0], ": ")[1]
+        version, _ = strconv.Unquote(version)
+    }
+
+    return
+}
+
+func (h *Helm) GetState(out string) (status string) {
+    re := regexp.MustCompile(`STATUS: .*`)
+    result := re.FindStringSubmatch(string(out))
+    if result != nil {
+        status = strings.ToLower(strings.Split(result[0], ": ")[1])
+    }
+
+    return
+}
+
+func (h *Helm) GetAddress(out string) (ip, port string) {
+    var tmp string
+    re := regexp.MustCompile(`ClusterIP.*`)
+    addr := re.FindStringSubmatch(string(out))
+    if addr != nil {
+        fmt.Sscanf(addr[0], "%s %s %s %s", &tmp, &ip, &tmp, &port)
+    }
+
+    return
+}
+
+func (h *Helm) GetNames(out string) (names []string, err error) {
+    re := regexp.MustCompile(`Name: .*`)
+    result := re.FindAllStringSubmatch(out, -1)
+    if result == nil {
+        return
+    }
+
+    for _, v := range result {
+        xappName := strings.Split(v[0], ": ")[1]
+        if strings.Contains(xappName, "appmgr") == false {
+            names = append(names, xappName)
+        }
+    }
+    return names, nil
+}
+
+func (h *Helm) FillInstanceData(name string, out string, xapp *Xapp, msgs MessageTypes) {
+    ip, port := h.GetAddress(out)
+
+    var tmp string
+    r := regexp.MustCompile(`(?s)\/Pod.*?\/Service`)
+    result := r.FindStringSubmatch(string(out))
+    if result == nil {
+        return
+    }
+
+    re := regexp.MustCompile(name + "-(\\d+).*")
+    resources := re.FindAllStringSubmatch(string(result[0]), -1)
+    if resources != nil {
+        for _, v := range resources {
+            var x XappInstance
+            fmt.Sscanf(v[0], "%s %s %s", &x.Name, &tmp, &x.Status)
+            x.Status = strings.ToLower(x.Status)
+            x.Ip = ip
+            x.Port, _ = strconv.Atoi(strings.Split(port, "/")[0])
+            x.TxMessages = msgs.TxMessages
+            x.RxMessages = msgs.RxMessages
+            xapp.Instances = append(xapp.Instances, x)
+        }
+    }
+}
+
+func (h *Helm) ParseStatus(name string, out string) (xapp Xapp, err error) {
+    types, err := h.GetMessages(name)
+    if (err != nil) {
+        return
+    }
+
+    xapp.Name = name
+    xapp.Version = h.GetVersion(name)
+    xapp.Status = h.GetState(out)
+    h.FillInstanceData(name, out, &xapp, types)
+
+    return
+}
+
+func (h *Helm) parseAllStatus(names []string) (xapps []Xapp, err error) {
+    xapps = []Xapp{}
+
+    for _, name := range names {
+        x, err := h.Status(name)
+        if err == nil {
+            xapps = append(xapps, x)
+        }
+    }
+
+    return
+}
+
+func addTillerEnv() (err error) {
+
+    service := viper.GetString("helm.tiller-service")
+    namespace := viper.GetString("helm.tiller-namespace")
+    port := viper.GetString("helm.tiller-port")
+
+    if err = os.Setenv("HELM_HOST", service + "." + namespace + ":" + port); err != nil {
+        mdclog(Mdclog_err, formatLog("Tiller Env Setting Failed", "", err.Error()))
+    }
+
+    return err
+}
+
+func getNamespaceArgs() (string) {
+    ns := viper.GetString("xapp.namespace")
+    if ns == "" {
+        ns = "ricxapp"
+    }
+    return " --namespace=" + ns
+}
+
+func formatLog(text string, args string, err string) (string) {
+    return fmt.Sprintf("Helm: %s: args=%s err=%s\n", text, args, err)
+}
+
diff --git a/src/helm_test.go b/src/helm_test.go
new file mode 100755 (executable)
index 0000000..44882df
--- /dev/null
@@ -0,0 +1,277 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "fmt"
+    "os"
+    "os/exec"
+       "strconv"
+    "testing"
+    "reflect"
+    "github.com/spf13/viper"
+    "io/ioutil"
+    "path"
+)
+
+var helmStatusOutput = `
+LAST DEPLOYED: Sat Mar  9 06:50:45 2019
+NAMESPACE: default
+STATUS: DEPLOYED
+
+RESOURCES:
+==> v1/Pod(related)
+NAME                        READY  STATUS   RESTARTS  AGE
+dummy-xapp-8984fc9fd-bkcbp  1/1    Running  0         55m
+dummy-xapp-8984fc9fd-l6xch  1/1    Running  0         55m
+dummy-xapp-8984fc9fd-pp4hg  1/1    Running  0         55m
+
+==> v1/Service
+NAME                         TYPE       CLUSTER-IP      EXTERNAL-IP  PORT(S)  AGE
+dummy-xapp-dummy-xapp-chart  ClusterIP  10.102.184.212  <none>       80/TCP   55m
+
+==> v1beta1/Deployment
+NAME        READY  UP-TO-DATE  AVAILABLE  AGE
+dummy-xapp  3/3    3           3          55m
+`
+
+var helListOutput = `Next: ""
+Releases:
+- AppVersion: "1.0"
+  Chart: dummy-xapp-chart-0.1.0
+  Name: dummy-xapp
+  Namespace: default
+  Revision: 1
+  Status: DEPLOYED
+  Updated: Mon Mar 11 06:55:05 2019
+- AppVersion: "2.0"
+  Chart: dummy-xapp-chart-0.1.0
+  Name: dummy-xapp2
+  Namespace: default
+  Revision: 1
+  Status: DEPLOYED
+  Updated: Mon Mar 11 06:55:05 2019
+- AppVersion: "1.0"
+  Chart: appmgr-0.0.1
+  Name: appmgr
+  Namespace: default
+  Revision: 1
+  Status: DEPLOYED
+  Updated: Sun Mar 24 07:17:00 2019`
+
+var mockedExitStatus = 0
+var mockedStdout string
+var h = Helm{}
+
+func fakeExecCommand(command string, args ...string) *exec.Cmd {
+    cs := []string{"-test.run=TestExecCommandHelper", "--", command}
+    cs = append(cs, args...)
+       
+       cmd := exec.Command(os.Args[0], cs...)
+    es := strconv.Itoa(mockedExitStatus)
+       cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1", "STDOUT=" + mockedStdout, "EXIT_STATUS=" + es}
+       
+    return cmd
+}
+
+func TestExecCommandHelper(t *testing.T) {
+    if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+        return
+    }
+
+    fmt.Fprintf(os.Stdout, os.Getenv("STDOUT"))
+    i, _ := strconv.Atoi(os.Getenv("EXIT_STATUS"))
+    os.Exit(i)
+}
+
+func writeTestCreds() (err error) {
+
+    // Write test entries to helm username and password files
+    f, err := os.Create(viper.GetString("helm.helm-username-file"))
+    if err != nil {
+        return err
+    }
+
+    _, err = f.WriteString(viper.GetString("helm.secrets.username"))
+    if err != nil {
+        f.Close()
+        return (err)
+    }
+    f.Close()
+
+    f, err = os.Create(viper.GetString("helm.helm-password-file"))
+    if err != nil {
+        return err
+    }
+
+    _, err = f.WriteString(viper.GetString("helm.secrets.password"))
+    if err != nil {
+        f.Close()
+        return (err)
+    }
+    f.Close()
+    return
+}
+
+func TestHelmInit(t *testing.T) {
+       mockedExitStatus = 0
+    execCommand = fakeExecCommand
+    defer func() { execCommand = exec.Command }()
+
+    if err := writeTestCreds(); err != nil {
+        t.Errorf("Writing test entries failed: %s", err)
+        return
+    }
+
+    out, err := h.Init()
+    if err != nil {
+        t.Errorf("Helm init failed: %s %s", err, string(out))
+    }
+}
+
+func TestHelmInstall(t *testing.T) {
+    copyFile(t)
+    mockedExitStatus = 0
+       execCommand = fakeExecCommand
+       mockedStdout = helmStatusOutput
+    defer func() { execCommand = exec.Command }()
+
+    xapp, err := h.Install("dummy-xapp")
+    if err != nil {
+        t.Errorf("Helm install failed: %v", err)
+       }
+
+    x := getXappData()
+    xapp.Version = "1.0"
+
+    if !reflect.DeepEqual(xapp, x) {
+        t.Errorf("%v \n%v", xapp, x)
+    }
+}
+
+func TestHelmStatus(t *testing.T) {
+    copyFile(t)
+    mockedExitStatus = 0
+    mockedStdout = helmStatusOutput
+    execCommand = fakeExecCommand
+    defer func() { execCommand = exec.Command }()
+
+    xapp, err := h.Status("dummy-xapp")
+    if err != nil {
+        t.Errorf("Helm status failed: %v", err)
+       }
+
+    x := getXappData()
+    xapp.Version = "1.0"
+
+       if !reflect.DeepEqual(xapp, x) {
+        t.Errorf("%v \n%v", xapp, x)
+    }
+}
+
+func TestHelmStatusAll(t *testing.T) {
+    copyFile(t)
+    mockedExitStatus = 0
+    mockedStdout = helListOutput
+    execCommand = fakeExecCommand
+    defer func() { execCommand = exec.Command }()
+
+    xapp, err := h.StatusAll()
+    if err != nil {
+        t.Errorf("Helm StatusAll failed: %v - %v", err, xapp)
+       }
+
+    // Todo: check the content
+}
+
+func TestHelmParseAllStatus(t *testing.T) {
+    copyFile(t)
+    mockedExitStatus = 0
+    mockedStdout = helListOutput
+    execCommand = fakeExecCommand
+    defer func() { execCommand = exec.Command }()
+
+    xapp, err := h.parseAllStatus([]string{"dummy-xapp", "dummy-xapp2"})
+    if err != nil {
+        t.Errorf("Helm parseAllStatus failed: %v - %v", err, xapp)
+       }
+
+    // Todo: check the content
+}
+
+func TestHelmDelete(t *testing.T) {
+    copyFile(t)
+    mockedExitStatus = 0
+    mockedStdout = helListOutput
+    execCommand = fakeExecCommand
+    defer func() { execCommand = exec.Command }()
+
+    xapp, err := h.Delete("dummy-xapp")
+    if err != nil {
+        t.Errorf("Helm delete failed: %v - %v", err, xapp)
+       }
+
+    // Todo: check the content
+}
+
+func TestHelmLists(t *testing.T) {
+    mockedExitStatus = 0
+    mockedStdout = helListOutput
+    execCommand = fakeExecCommand
+    defer func() { execCommand = exec.Command }()
+
+    names, err := h.List()
+    if err != nil {
+        t.Errorf("Helm status failed: %v", err)
+       }
+
+    if !reflect.DeepEqual(names, []string{"dummy-xapp", "dummy-xapp2"}) {
+        t.Errorf("Helm status failed: %v", err)
+    }
+}
+
+func getXappData() (x Xapp) {
+    x = generateXapp("dummy-xapp", "deployed", "1.0", "dummy-xapp-8984fc9fd-bkcbp", "running", "10.102.184.212", "80")
+    x.Instances = append(x.Instances, x.Instances[0])
+    x.Instances = append(x.Instances, x.Instances[0])
+    x.Instances[1].Name = "dummy-xapp-8984fc9fd-l6xch"
+    x.Instances[2].Name = "dummy-xapp-8984fc9fd-pp4hg"
+
+    return x
+}
+
+
+func copyFile(t *testing.T) {
+    tarDir := path.Join(viper.GetString("xapp.tarDir"), "dummy-xapp")
+    err := os.MkdirAll(tarDir, 0777)
+    if err != nil {
+         t.Errorf("%v", err)
+    }
+
+    data, err := ioutil.ReadFile("../config/msg_type.yaml")
+    if err != nil {
+         t.Errorf("%v", err)
+    }
+
+    _ = ioutil.WriteFile(path.Join(tarDir, "msg_type.yaml"), data, 0644)
+    if err != nil {
+         t.Errorf("%v", err)
+    }
+}
\ No newline at end of file
diff --git a/src/logger.go b/src/logger.go
new file mode 100755 (executable)
index 0000000..bc255ee
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+/*
+#cgo CFLAGS: -I/usr/local/include
+#cgo LDFLAGS: -lmdclog
+#
+#include <mdclog/mdclog.h>
+void xAppMgr_mdclog_write(mdclog_severity_t severity, const char *msg) {
+     mdclog_write(severity, "%s", msg);
+}
+*/
+import "C"
+
+import (
+    "net/http"
+    "time"
+    "fmt"
+)
+
+func mdclog(severity C.mdclog_severity_t, msg string) {
+    msg = fmt.Sprintf("%s:: %s ", time.Now().Format("2019-01-02 15:04:05"), msg)
+
+    C.mdclog_mdc_add(C.CString("XM"), C.CString("1.0.0"))
+    C.xAppMgr_mdclog_write(severity, C.CString(msg))
+}
+
+func Logger(inner http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        inner.ServeHTTP(w, r)
+        s := fmt.Sprintf("Logger: method=%s url=%s", r.Method, r.URL.RequestURI())
+        mdclog(C.MDCLOG_DEBUG, s)
+    })
+}
diff --git a/src/main.go b/src/main.go
new file mode 100755 (executable)
index 0000000..e948cac
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+func main() {
+    loadConfig()
+
+    m := XappManager{}
+    m.Initialize(&Helm{})
+
+    m.Run()
+}
diff --git a/src/subscriptions.go b/src/subscriptions.go
new file mode 100755 (executable)
index 0000000..c8db1e6
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "bytes"
+    "log"
+    "net/http"
+    "encoding/json"
+    "time"
+    "github.com/segmentio/ksuid"
+    cmap "github.com/orcaman/concurrent-map"
+)
+
+func (sd *SubscriptionDispatcher) Initialize() {
+    sd.client = &http.Client{}
+    sd.subscriptions = cmap.New()
+}
+
+func (sd *SubscriptionDispatcher) Add(sr SubscriptionReq) (resp SubscriptionResp) {
+    key := ksuid.New().String()
+    resp = SubscriptionResp{key, 0, sr.EventType}
+    sr.Id = key
+
+    sd.subscriptions.Set(key, Subscription{sr, resp})
+
+    log.Printf("New subscription added: key=%s value=%v", key, sr)
+    return
+}
+
+func (sd *SubscriptionDispatcher) GetAll() (hooks []SubscriptionReq) {
+    hooks = []SubscriptionReq{}
+    for v := range sd.subscriptions.IterBuffered() {
+        hooks = append(hooks, v.Val.(Subscription).req)
+    }
+
+    return hooks
+}
+
+func (sd *SubscriptionDispatcher) Get(id string) (SubscriptionReq, bool) {
+    if v, found := sd.subscriptions.Get(id); found {
+        log.Printf("Subscription id=%s found: %v", id, v.(Subscription).req)
+
+        return v.(Subscription).req, found
+    }
+    return SubscriptionReq{}, false
+}
+
+func (sd *SubscriptionDispatcher) Delete(id string) (SubscriptionReq, bool) {
+    if v, found := sd.subscriptions.Get(id); found {
+        log.Printf("Subscription id=%s found: %v ... deleting", id, v.(Subscription).req)
+
+        sd.subscriptions.Remove(id)
+        return v.(Subscription).req, found
+    }
+    return SubscriptionReq{}, false
+}
+
+func (sd *SubscriptionDispatcher) Update(id string, sr SubscriptionReq) (SubscriptionReq, bool) {
+    if s, found := sd.subscriptions.Get(id); found {
+        log.Printf("Subscription id=%s found: %v ... updating", id, s.(Subscription).req)
+
+        sr.Id = id
+        sd.subscriptions.Set(id, Subscription{sr, s.(Subscription).resp});
+        return sr, found
+    }
+    return SubscriptionReq{}, false
+}
+
+func (sd *SubscriptionDispatcher) Publish(x Xapp, et EventType) {
+    for v := range sd.subscriptions.Iter() {
+        go sd.notify(x, et, v.Val.(Subscription))
+    }
+}
+
+func (sd *SubscriptionDispatcher) notify(x Xapp, et EventType, s Subscription) error {
+    notif := []SubscriptionNotif{}
+    notif = append(notif, SubscriptionNotif{Id: s.req.Id, Version: s.resp.Version, EventType: string(et), XappData: x})
+
+    jsonData, err := json.Marshal(notif)
+    if err != nil {
+        log.Panic(err)
+    }
+
+    // Execute the request with retry policy
+    return sd.retry(s, func() error {
+        resp, err := http.Post(s.req.TargetUrl, "application/json", bytes.NewBuffer(jsonData))
+        if err != nil {
+            log.Printf("Posting to subscription failed: %v", err)
+            return err
+        }
+
+        if resp.StatusCode != http.StatusOK {
+            log.Printf("Client returned error code: %d", resp.StatusCode)
+            return err
+        }
+
+        log.Printf("subscription to '%s' dispatched, response code: %d \n", s.req.TargetUrl, resp.StatusCode)
+        return nil
+    })
+}
+
+func (sd *SubscriptionDispatcher) retry(s Subscription, fn func() error) error {
+    if err := fn(); err != nil {
+        // Todo: use exponential backoff, or similar mechanism
+        if s.req.MaxRetries--; s.req.MaxRetries > 0 {
+            time.Sleep(time.Duration(s.req.RetryTimer) * time.Second)
+            return sd.retry(s, fn)
+        }
+        sd.subscriptions.Remove(s.req.Id);
+        return err
+    }
+    return nil
+}
diff --git a/src/subscriptions_test.go b/src/subscriptions_test.go
new file mode 100755 (executable)
index 0000000..065eddb
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "net/http"
+    "testing"
+    "bytes"
+    "encoding/json"
+    "net/http/httptest"
+    "net"
+    "log"
+    "fmt"
+)
+
+var resp SubscriptionResp
+
+// Test cases
+func TestNoSubscriptionsFound(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/subscriptions", nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusOK, response.Code)
+    if body := response.Body.String(); body != "[]" {
+        t.Errorf("handler returned unexpected body: got %v want []", body)
+    }
+}
+
+func TestAddNewSubscription(t *testing.T) {
+    payload := []byte(`{"maxRetries": 3, "retryTimer": 5, "eventType":"Created", "targetUrl": "http://localhost:8087/xapps_handler"}`)
+    req, _ := http.NewRequest("POST", "/ric/v1/subscriptions", bytes.NewBuffer(payload))
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusCreated, response.Code)
+
+    json.NewDecoder(response.Body).Decode(&resp)
+    if resp.Version != 0 {
+        t.Errorf("Creating new subscription failed: %v", resp)
+    }
+}
+
+func TestGettAllSubscriptions(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/subscriptions", nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusOK, response.Code)
+
+    var subscriptions []SubscriptionReq
+    json.NewDecoder(response.Body).Decode(&subscriptions)
+
+    verifySubscription(t, subscriptions[0], "http://localhost:8087/xapps_handler", 3, 5, "Created")
+}
+
+func TestGetSingleSubscription(t *testing.T) {
+    req, _ := http.NewRequest("GET", "/ric/v1/subscriptions/" + resp.Id, nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusOK, response.Code)
+
+    var subscription SubscriptionReq
+    json.NewDecoder(response.Body).Decode(&subscription)
+
+    verifySubscription(t, subscription, "http://localhost:8087/xapps_handler", 3, 5, "Created")
+}
+
+func TestUpdateSingleSubscription(t *testing.T) {
+    payload := []byte(`{"maxRetries": 11, "retryTimer": 22, "eventType":"Deleted", "targetUrl": "http://localhost:8088/xapps_handler"}`)
+
+    req, _ := http.NewRequest("PUT", "/ric/v1/subscriptions/" + resp.Id, bytes.NewBuffer(payload))
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusOK, response.Code)
+
+    var res SubscriptionResp
+    json.NewDecoder(response.Body).Decode(&res)
+    if res.Version != 0 {
+        t.Errorf("handler returned unexpected data: %v", resp)
+    }
+
+    // Check that the subscription is updated properly
+    req, _ = http.NewRequest("GET", "/ric/v1/subscriptions/" + resp.Id, nil)
+    response = executeRequest(req)
+    checkResponseCode(t, http.StatusOK, response.Code)
+
+    var subscription SubscriptionReq
+    json.NewDecoder(response.Body).Decode(&subscription)
+
+    verifySubscription(t, subscription, "http://localhost:8088/xapps_handler", 11, 22, "Deleted")
+}
+
+func TestDeleteSingleSubscription(t *testing.T) {
+    req, _ := http.NewRequest("DELETE", "/ric/v1/subscriptions/" + resp.Id, nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusNoContent, response.Code)
+
+    // Check that the subscription is removed properly
+    req, _ = http.NewRequest("GET", "/ric/v1/subscriptions/" + resp.Id, nil)
+    response = executeRequest(req)
+    checkResponseCode(t, http.StatusNotFound, response.Code)
+}
+
+func TestDeleteSingleSubscriptionFails(t *testing.T) {
+    req, _ := http.NewRequest("DELETE", "/ric/v1/subscriptions/invalidSubscriptionId" , nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusNotFound, response.Code)
+}
+
+func TestAddSingleSubscriptionFailsBodyEmpty(t *testing.T) {
+    req, _ := http.NewRequest("POST", "/ric/v1/subscriptions/" + resp.Id , nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusMethodNotAllowed, response.Code)
+}
+
+func TestUpdateeSingleSubscriptionFailsBodyEmpty(t *testing.T) {
+    req, _ := http.NewRequest("PUT", "/ric/v1/subscriptions/" + resp.Id , nil)
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusMethodNotAllowed, response.Code)
+}
+
+func TestUpdateeSingleSubscriptionFailsInvalidId(t *testing.T) {
+    payload := []byte(`{"maxRetries": 11, "retryTimer": 22, "eventType":"Deleted", "targetUrl": "http://localhost:8088/xapps_handler"}`)
+
+    req, _ := http.NewRequest("PUT", "/ric/v1/subscriptions/invalidSubscriptionId" + resp.Id, bytes.NewBuffer(payload))
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusNotFound, response.Code)
+}
+
+func TestPublishXappAction(t *testing.T) {
+    payload := []byte(`{"maxRetries": 3, "retryTimer": 5, "eventType":"Created", "targetUrl": "http://127.0.0.1:8888"}`)
+    req, _ := http.NewRequest("POST", "/ric/v1/subscriptions", bytes.NewBuffer(payload))
+    response := executeRequest(req)
+
+    checkResponseCode(t, http.StatusCreated, response.Code)
+
+    // Create a RestApi server (simulating RM)
+    ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               fmt.Fprintln(w, "Hello, XM!")
+    }))
+
+    l, err := net.Listen("tcp", "127.0.0.1:8888")
+    if err != nil {
+        log.Fatal(err)
+    }
+    ts.Listener.Close()
+    ts.Listener = l
+    ts.Start()
+
+    defer ts.Close()
+
+    x.sd.Publish(xapp, EventType("created"))
+}
+
+func verifySubscription(t *testing.T, subscription SubscriptionReq, url string, retries int, timer int, event string) {
+    if subscription.TargetUrl != url {
+        t.Errorf("Unexpected url: got=%s expected=%s", subscription.TargetUrl, url)
+    }
+
+    if subscription.MaxRetries != retries {
+        t.Errorf("Unexpected retries: got=%d expected=%d", subscription.MaxRetries, retries)
+    }
+
+    if subscription.RetryTimer != timer {
+        t.Errorf("Unexpected timer: got=%d expected=%d", subscription.RetryTimer, timer)
+    }
+
+    if subscription.EventType != event {
+        t.Errorf("Unexpected event type: got=%s expected=%s", subscription.EventType, event)
+    }
+}
\ No newline at end of file
diff --git a/src/types.go b/src/types.go
new file mode 100755 (executable)
index 0000000..930d008
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+==================================================================================
+  Copyright (c) 2019 AT&T Intellectual Property.
+  Copyright (c) 2019 Nokia
+
+   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 main
+
+import (
+    "github.com/gorilla/mux"
+    cmap "github.com/orcaman/concurrent-map"
+    "net/http"
+)
+
+type CmdOptions struct {
+    hostAddr      *string
+    helmHost      *string
+    helmChartPath *string
+}
+
+type Resource struct {
+    Method      string
+    Url         string
+    HandlerFunc http.HandlerFunc
+}
+
+type Xapp struct {
+    Name        string    `json:"name"`
+    Status      string    `json:"status"`
+    Version     string    `json:"version"`
+    Instances   []XappInstance `json:"instances"`
+}
+
+type XappInstance struct {
+    Name        string  `json:"name"`
+    Status      string  `json:"status"`
+    Ip          string  `json:"ip"`
+    Port        int     `json:"port"`
+    TxMessages  []string  `json:"txMessages"`
+    RxMessages  []string  `json:"rxMessages"`
+}
+
+type XappManager struct {
+    router *mux.Router
+    helm   Helmer
+    sd     SubscriptionDispatcher
+    opts   CmdOptions
+}
+
+type Helmer interface {
+    Install(name string) (xapp Xapp, err error)
+    Status(name string) (xapp Xapp, err error)
+    StatusAll() (xapps []Xapp, err error)
+    List() (xapps []string, err error)
+    Delete(name string) (xapp Xapp, err error)
+}
+
+type Helm struct {
+    host      string
+    chartPath string
+    initDone  bool
+}
+
+type SubscriptionReq struct {
+    Id          string `json:"id"`
+    TargetUrl   string `json:"targetUrl"`
+    EventType   string `json:"eventType"`
+    MaxRetries  int    `json:"maxRetries"`
+    RetryTimer  int    `json:"retryTimer"`
+}
+
+type SubscriptionResp struct {
+    Id          string `json:"id"`
+    Version     int    `json:"version"`
+    EventType   string `json:"eventType"`
+}
+
+type SubscriptionNotif struct {
+    Id          string `json:"id"`
+    Version     int    `json:"version"`
+    EventType   string `json:"eventType"`
+    XappData    Xapp   `json:"xapp"`
+}
+
+type Subscription struct {
+    req     SubscriptionReq
+    resp    SubscriptionResp
+}
+
+type SubscriptionDispatcher struct {
+    client          *http.Client
+    subscriptions   cmap.ConcurrentMap
+}
+
+type MessageTypes struct {
+       TxMessages []string `yaml:"txMessages"`
+       RxMessages []string `yaml:"rxMessages"`
+}
+
+type EventType string
+
+const (
+    Created     EventType = "created"
+    Updated     EventType = "updated"
+    Deleted     EventType = "deleted"
+)
+
+const (
+    Mdclog_err     = 1 //! Error level log entry
+    Mdclog_warn    = 2 //! Warning level log entry
+    Mdclog_info    = 3 //! Info level log entry
+    Mdclog_debug   = 4 //! Debug level log entry
+)