From 8504f6a10b6417bd6b73249941c8f6356442fc9b Mon Sep 17 00:00:00 2001 From: Abukar Mohamed Date: Wed, 3 Apr 2019 11:07:48 +0000 Subject: [PATCH] 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 --- LICENSES.txt | 28 ++ README.md | 390 ++++++++++++++++++++ build/Dockerfile | 165 +++++++++ build/Makefile | 129 +++++++ build/docker-entrypoint.sh | 24 ++ cli/appmgrcli | 363 ++++++++++++++++++ config/appmgr.yaml | 33 ++ config/msg_type.yaml | 21 ++ helm_chart/appmgr/Chart.yaml | 20 + helm_chart/appmgr/templates/_helpers.tpl | 47 +++ helm_chart/appmgr/templates/appconfig.yaml | 24 ++ helm_chart/appmgr/templates/appenv.yaml | 23 ++ helm_chart/appmgr/templates/deployment.yaml | 96 +++++ helm_chart/appmgr/templates/service.yaml | 35 ++ helm_chart/appmgr/values.yaml | 114 ++++++ rest_api/xapp_manager_rest_api.json | 545 ++++++++++++++++++++++++++++ src/api.go | 251 +++++++++++++ src/api_test.go | 265 ++++++++++++++ src/config.go | 56 +++ src/helm.go | 359 ++++++++++++++++++ src/helm_test.go | 277 ++++++++++++++ src/logger.go | 51 +++ src/main.go | 29 ++ src/subscriptions.go | 131 +++++++ src/subscriptions_test.go | 191 ++++++++++ src/types.go | 126 +++++++ 26 files changed, 3793 insertions(+) create mode 100755 LICENSES.txt create mode 100755 README.md create mode 100755 build/Dockerfile create mode 100755 build/Makefile create mode 100755 build/docker-entrypoint.sh create mode 100755 cli/appmgrcli create mode 100755 config/appmgr.yaml create mode 100755 config/msg_type.yaml create mode 100755 helm_chart/appmgr/Chart.yaml create mode 100755 helm_chart/appmgr/templates/_helpers.tpl create mode 100755 helm_chart/appmgr/templates/appconfig.yaml create mode 100755 helm_chart/appmgr/templates/appenv.yaml create mode 100755 helm_chart/appmgr/templates/deployment.yaml create mode 100755 helm_chart/appmgr/templates/service.yaml create mode 100755 helm_chart/appmgr/values.yaml create mode 100644 rest_api/xapp_manager_rest_api.json create mode 100755 src/api.go create mode 100755 src/api_test.go create mode 100755 src/config.go create mode 100755 src/helm.go create mode 100755 src/helm_test.go create mode 100755 src/logger.go create mode 100755 src/main.go create mode 100755 src/subscriptions.go create mode 100755 src/subscriptions_test.go create mode 100755 src/types.go 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 < /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 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 +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 +) -- 2.16.6