Initial version with full functionality
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>
diff --git a/LICENSES.txt b/LICENSES.txt
new file mode 100755
index 0000000..5c81246
--- /dev/null
+++ b/LICENSES.txt
@@ -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
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
index 0000000..f76e31d
--- /dev/null
+++ b/build/Dockerfile
@@ -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
index 0000000..266ac05
--- /dev/null
+++ b/build/Makefile
@@ -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
index 0000000..80e99df
--- /dev/null
+++ b/build/docker-entrypoint.sh
@@ -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
index 0000000..e31048a
--- /dev/null
+++ b/cli/appmgrcli
@@ -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
index 0000000..3753f1f
--- /dev/null
+++ b/config/appmgr.yaml
@@ -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
index 0000000..c700250
--- /dev/null
+++ b/config/msg_type.yaml
@@ -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
index 0000000..36bb213
--- /dev/null
+++ b/helm_chart/appmgr/Chart.yaml
@@ -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
index 0000000..9e266be
--- /dev/null
+++ b/helm_chart/appmgr/templates/_helpers.tpl
@@ -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
index 0000000..b31e1c5
--- /dev/null
+++ b/helm_chart/appmgr/templates/appconfig.yaml
@@ -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
index 0000000..ccda649
--- /dev/null
+++ b/helm_chart/appmgr/templates/appenv.yaml
@@ -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
index 0000000..0ef46d2
--- /dev/null
+++ b/helm_chart/appmgr/templates/deployment.yaml
@@ -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
index 0000000..b89dc64
--- /dev/null
+++ b/helm_chart/appmgr/templates/service.yaml
@@ -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
index 0000000..8d71678
--- /dev/null
+++ b/helm_chart/appmgr/values.yaml
@@ -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
index 0000000..951f199
--- /dev/null
+++ b/rest_api/xapp_manager_rest_api.json
@@ -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
index 0000000..b9c2b63
--- /dev/null
+++ b/src/api.go
@@ -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
index 0000000..7610f49
--- /dev/null
+++ b/src/api_test.go
@@ -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
index 0000000..e9ede6b
--- /dev/null
+++ b/src/config.go
@@ -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
index 0000000..cfa1f18
--- /dev/null
+++ b/src/helm.go
@@ -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
index 0000000..44882df
--- /dev/null
+++ b/src/helm_test.go
@@ -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
index 0000000..bc255ee
--- /dev/null
+++ b/src/logger.go
@@ -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
index 0000000..e948cac
--- /dev/null
+++ b/src/main.go
@@ -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
index 0000000..c8db1e6
--- /dev/null
+++ b/src/subscriptions.go
@@ -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
index 0000000..065eddb
--- /dev/null
+++ b/src/subscriptions_test.go
@@ -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
index 0000000..930d008
--- /dev/null
+++ b/src/types.go
@@ -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
+)