From 34e4383c438f87023bc411d9b0baa4a828a7e306 Mon Sep 17 00:00:00 2001 From: Mohamed Abukar Date: Wed, 13 Nov 2019 17:57:15 +0200 Subject: [PATCH] Swagger-baser server REST API interface Change-Id: Ic2057a93bd9dfbb19c3b00cc497a4243120c6662 Signed-off-by: Mohamed Abukar --- Dockerfile | 30 +- api/appmgr_rest_api.json | 840 ----------------- api/appmgr_rest_api.yaml | 249 +++-- build/make.go.mk | 4 +- cmd/{appmgr/main.go => appmgr.go} | 15 +- cmd/appmgr/api_test.go | 313 ------ cmd/appmgr/db.go | 91 -- cmd/appmgr/subscriptions.go | 162 ---- cmd/appmgr/subscriptions_test.go | 198 ---- cmd/appmgr/types.go | 159 ---- container-tag.yaml | 2 +- go.mod | 11 + go.sum | 120 +++ cmd/appmgr/config.go => pkg/appmgr/appmgr.go | 32 +- pkg/appmgr/types.go | 90 ++ cmd/appmgr/desc.go => pkg/cm/cm.go | 157 ++-- cmd/appmgr/desc_test.go => pkg/cm/cm_test.go | 138 ++- {cmd/appmgr => pkg/helm}/helm.go | 210 ++--- {cmd/appmgr => pkg/helm}/helm_test.go | 99 +- {cmd/appmgr => pkg/logger}/logger.go | 10 +- pkg/restful/restful.go | 245 +++++ pkg/restful/types.go | 49 + pkg/resthooks/resthooks.go | 224 +++++ pkg/resthooks/resthooks_test.go | 124 +++ pkg/resthooks/types.go | 41 + pkg/util/util.go | 68 ++ scripts/appmgrcli | 1305 +++++++++++++++++++------- 27 files changed, 2443 insertions(+), 2543 deletions(-) mode change 100644 => 100755 Dockerfile delete mode 100644 api/appmgr_rest_api.json mode change 100644 => 100755 build/make.go.mk rename cmd/{appmgr/main.go => appmgr.go} (83%) delete mode 100755 cmd/appmgr/api_test.go delete mode 100755 cmd/appmgr/db.go delete mode 100755 cmd/appmgr/subscriptions.go delete mode 100755 cmd/appmgr/subscriptions_test.go delete mode 100755 cmd/appmgr/types.go mode change 100644 => 100755 container-tag.yaml rename cmd/appmgr/config.go => pkg/appmgr/appmgr.go (75%) create mode 100755 pkg/appmgr/types.go rename cmd/appmgr/desc.go => pkg/cm/cm.go (51%) rename cmd/appmgr/desc_test.go => pkg/cm/cm_test.go (59%) rename {cmd/appmgr => pkg/helm}/helm.go (57%) rename {cmd/appmgr => pkg/helm}/helm_test.go (53%) rename {cmd/appmgr => pkg/logger}/logger.go (87%) create mode 100755 pkg/restful/restful.go create mode 100755 pkg/restful/types.go create mode 100755 pkg/resthooks/resthooks.go create mode 100755 pkg/resthooks/resthooks_test.go create mode 100755 pkg/resthooks/types.go create mode 100755 pkg/util/util.go diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index adc6a04..027e372 --- a/Dockerfile +++ b/Dockerfile @@ -32,23 +32,33 @@ RUN wget -nv https://storage.googleapis.com/kubernetes-helm/helm-${HELMVERSION}- # Install kubectl from Docker Hub COPY --from=lachlanevenson/k8s-kubectl:v1.10.3 /usr/local/bin/kubectl /usr/local/bin/kubectl -RUN mkdir -p /ws -WORKDIR "/ws" ENV GOPATH="/go" +# Swagger +RUN mkdir -p /go/bin +RUN cd /go/bin \ + && wget --quiet https://github.com/go-swagger/go-swagger/releases/download/v0.19.0/swagger_linux_amd64 \ + && mv swagger_linux_amd64 swagger \ + && chmod +x swagger + +RUN mkdir -p /go/src/ws +WORKDIR "/go/src/ws" + # Module prepare (if go.mod/go.sum updated) -COPY go.mod /ws -COPY go.sum /ws +COPY go.mod /go/src/ws +COPY go.sum /go/src/ws RUN GO111MODULE=on go mod download # build and test -COPY . /ws +COPY . /go/src/ws + +RUN /go/bin/swagger generate server -f api/appmgr_rest_api.yaml --name AppManager -t pkg/ --exclude-main -RUN make -C /ws go-build +COPY . /go/src/ws -RUN make -C /ws go-test-fmt +RUN make -C /go/src/ws go-build -#RUN make -C /ws go-test +RUN make -C /go/src/ws go-test-fmt CMD ["/bin/bash"] @@ -70,12 +80,12 @@ COPY --from=appmgr-build /usr/local/bin/kubectl /usr/local/bin/kubectl RUN ldconfig # -# xApp +# xApp Manager # RUN mkdir -p /opt/xAppManager \ && chmod -R 755 /opt/xAppManager -COPY --from=appmgr-build /ws/cache/go/cmd/appmgr /opt/xAppManager/appmgr +COPY --from=appmgr-build /go/src/ws/cache/go/cmd/appmgr /opt/xAppManager/appmgr WORKDIR /opt/xAppManager diff --git a/api/appmgr_rest_api.json b/api/appmgr_rest_api.json deleted file mode 100644 index 2b08195..0000000 --- a/api/appmgr_rest_api.json +++ /dev/null @@ -1,840 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "description": "This is a draft API for RIC appmgr", - "version": "0.1.7", - "title": "RIC appmgr", - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - } - }, - "host": "hostname", - "basePath": "/ric/v1", - "schemes": [ - "http" - ], - "paths": { - "/health/alive": { - "get": { - "summary": "Health check of xApp Manager - Liveness probe", - "tags": [ - "health" - ], - "operationId": "getHealthAlive", - "responses": { - "200": { - "description": "Status of xApp Manager is ok" - } - } - } - }, - "/health/ready": { - "get": { - "summary": "Readiness check of xApp Manager - Readiness probe", - "tags": [ - "health" - ], - "operationId": "getHealthReady", - "responses": { - "200": { - "description": "xApp Manager is ready for service" - }, - "503": { - "description": "xApp Manager is not ready for service" - } - } - } - }, - "/xapps": { - "post": { - "summary": "Deploy a xapp", - "tags": [ - "xapp" - ], - "operationId": "deployXapp", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "xAppInfo", - "in": "body", - "description": "xApp information", - "schema": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the xApp.", - "example": "xapp-dummy" - }, - "configName": { - "type": "string", - "description": "Name of the xApp configmap. Overrides the value given in Helm chart value file.", - "example": "xapp-dummy-configmap" - }, - "namespace": { - "type": "string", - "description": "Name of the namespace to which xApp is deployed. Overrides the value given in Helm chart value file.", - "example": "ricxapps" - }, - "serviceName": { - "type": "string", - "description": "Name of the service xApp is providing. Overrides the value given in Helm chart value file.", - "example": "xapp-dummy-service" - }, - "imageRepo": { - "type": "string", - "description": "Name of the docker repository xApp is located. Overrides the value given in Helm chart value file.", - "example": "xapprepo" - }, - "hostname": { - "type": "string", - "description": "Hostname for the pod. Used by messaging library. Overrides the value given in Helm chart value file.", - "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", - "tags": [ - "xapp" - ], - "operationId": "getAllXapps", - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "successful query of xApps", - "schema": { - "$ref": "#/definitions/AllDeployedXapps" - } - }, - "500": { - "description": "Internal error" - } - } - } - }, - "/xapps/search": { - "get": { - "summary": "Returns the list of all deployable xapps", - "tags": [ - "xapp" - ], - "operationId": "listAllDeployableXapps", - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "successful list of deployable xApps", - "schema": { - "$ref": "#/definitions/AllDeployableXapps" - } - }, - "500": { - "description": "Internal error" - } - } - } - }, - "/xapps/{xAppName}": { - "get": { - "summary": "Returns the status of a given xapp", - "tags": [ - "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", - "tags": [ - "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" - } - } - } - }, - "/xapps/{xAppName}/instances/{xAppInstanceName}": { - "get": { - "summary": "Returns the status of a given xapp", - "tags": [ - "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" - } - } - } - }, - "/config": { - "post": { - "summary": "Create xApp config", - "tags": [ - "xapp" - ], - "operationId": "createXappConfig", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "XAppConfig", - "in": "body", - "description": "xApp config", - "schema": { - "$ref": "#/definitions/XAppConfig" - } - } - ], - "responses": { - "201": { - "description": "xApp config successfully created", - "schema": { - "$ref": "#/definitions/XAppConfig" - } - }, - "400": { - "description": "Invalid input" - }, - "422": { - "description": "Validation of configuration failed" - }, - "500": { - "description": "Internal error" - } - } - }, - "put": { - "summary": "Modify xApp config", - "tags": [ - "xapp" - ], - "operationId": "ModifyXappConfig", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "XAppConfig", - "in": "body", - "description": "xApp config", - "schema": { - "$ref": "#/definitions/XAppConfig" - } - } - ], - "responses": { - "200": { - "description": "xApp config successfully modified", - "schema": { - "$ref": "#/definitions/XAppConfig" - } - }, - "400": { - "description": "Invalid input" - }, - "422": { - "description": "Validation of configuration failed" - }, - "500": { - "description": "Internal error" - } - } - }, - "get": { - "summary": "Returns the configuration of all xapps", - "tags": [ - "xapp" - ], - "operationId": "getAllXappConfig", - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "successful query of xApp config", - "schema": { - "$ref": "#/definitions/AllXappConfig" - } - }, - "500": { - "description": "Internal error" - } - } - }, - "delete": { - "summary": "Delete xApp configuration", - "tags": [ - "xapp" - ], - "operationId": "deleteXappConfig", - "parameters": [ - { - "name": "ConfigMetadata", - "in": "body", - "description": "xApp configuration information", - "schema": { - "$ref": "#/definitions/ConfigMetadata" - } - } - ], - "responses": { - "204": { - "description": "Successful deletion of xApp config" - }, - "400": { - "description": "Invalid parameters supplied" - }, - "500": { - "description": "Internal error" - } - } - } - }, - "/subscriptions": { - "post": { - "summary": "Subscribe event", - "tags": [ - "xapp", - "subscriptions" - ], - "operationId": "addSubscription", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "subscriptionRequest", - "in": "body", - "description": "New subscription", - "required": true, - "schema": { - "$ref": "#/definitions/subscriptionRequest" - } - } - ], - "responses": { - "201": { - "description": "Subscription successfully created", - "schema": { - "$ref": "#/definitions/subscriptionResponse" - } - }, - "400": { - "description": "Invalid input" - } - } - }, - "get": { - "summary": "Returns all subscriptions", - "tags": [ - "xapp", - "subscriptions" - ], - "operationId": "getSubscriptions", - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "successful query of subscriptions", - "schema": { - "$ref": "#/definitions/allSubscriptions" - } - } - } - } - }, - "/subscriptions/{subscriptionId}": { - "get": { - "summary": "Returns the information of subscription", - "tags": [ - "xapp", - "subscriptions" - ], - "operationId": "getSubscriptionById", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "subscriptionId", - "in": "path", - "description": "ID of subscription", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/subscription" - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Subscription not found" - } - } - }, - "put": { - "summary": "Modify event subscription", - "tags": [ - "xapp", - "subscriptions" - ], - "operationId": "modifySubscription", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "subscriptionId", - "in": "path", - "description": "ID of subscription", - "required": true, - "type": "string" - }, - { - "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", - "tags": [ - "xapp", - "subscriptions" - ], - "description": "", - "operationId": "deleteSubscription", - "parameters": [ - { - "name": "subscriptionId", - "in": "path", - "description": "ID of subscription", - "required": true, - "type": "string" - } - ], - "responses": { - "204": { - "description": "Successful deletion of subscription" - }, - "400": { - "description": "Invalid subscription supplied" - } - } - } - } - }, - "definitions": { - "AllDeployableXapps": { - "type": "array", - "items": { - "type": "string", - "example": "xapp-dummy" - } - }, - "AllDeployedXapps": { - "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" - } - } - } - }, - "ConfigMetadata": { - "type": "object", - "required": [ - "name", - "configName", - "namespace" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the xApp", - "example": "xapp-dummy" - }, - "configName": { - "type": "string", - "description": "Name of the config map", - "example": "xapp-dummy-config-map" - }, - "namespace": { - "type": "string", - "description": "Name of the namespace", - "example": "ricxapp" - } - } - }, - "XAppConfig": { - "type": "object", - "required": [ - "metadata", - "descriptor", - "config" - ], - "properties": { - "metadata": { - "$ref": "#/definitions/ConfigMetadata" - }, - "descriptor": { - "type": "object", - "description": "Schema of configuration in JSON format" - }, - "config": { - "type": "object", - "description": "Configuration in JSON format" - } - } - }, - "AllXappConfig": { - "type": "array", - "items": { - "$ref": "#/definitions/XAppConfig" - } - }, - "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", - "updated", - "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", - "updated", - "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", - "updated" - ] - }, - "xApps": { - "$ref": "#/definitions/AllDeployedXapps" - } - } - } - } -} diff --git a/api/appmgr_rest_api.yaml b/api/appmgr_rest_api.yaml index 9e7c15b..04f4f0a 100644 --- a/api/appmgr_rest_api.yaml +++ b/api/appmgr_rest_api.yaml @@ -1,7 +1,7 @@ swagger: '2.0' info: description: This is a draft API for RIC appmgr - version: 0.1.7 + version: 0.2.0 title: RIC appmgr license: name: Apache 2.0 @@ -42,38 +42,11 @@ paths: produces: - application/json parameters: - - name: xAppInfo + - name: XappDescriptor in: body - description: xApp information + description: xApp deployment info schema: - type: object - required: - - name - properties: - name: - type: string - description: Name of the xApp. - example: xapp-dummy - configName: - type: string - description: Name of the xApp configmap. Overrides the value given in Helm chart value file. - example: xapp-dummy-configmap - namespace: - type: string - description: Name of the namespace to which xApp is deployed. Overrides the value given in Helm chart value file. - example: ricxapps - serviceName: - type: string - description: Name of the service xApp is providing. Overrides the value given in Helm chart value file. - example: xapp-dummy-service - imageRepo: - type: string - description: Name of the docker repository xApp is located. Overrides the value given in Helm chart value file. - example: xapprepo - hostname: - type: string - description: Hostname for the pod. Used by messaging library. Overrides the value given in Helm chart value file. - example: xapp-dummy + $ref: '#/definitions/XappDescriptor' responses: '201': description: xApp successfully created @@ -97,12 +70,12 @@ paths: $ref: '#/definitions/AllDeployedXapps' '500': description: Internal error - '/xapps/search': + /xapps/list: get: summary: Returns the list of all deployable xapps tags: - xapp - operationId: listAllDeployableXapps + operationId: listAllXapps produces: - application/json responses: @@ -112,7 +85,7 @@ paths: $ref: '#/definitions/AllDeployableXapps' '500': description: Internal error - '/xapps/{xAppName}': + /xapps/{xAppName}: get: summary: Returns the status of a given xapp tags: @@ -155,7 +128,7 @@ paths: description: Invalid xApp name supplied '500': description: Internal error - '/xapps/{xAppName}/instances/{xAppInstanceName}': + /xapps/{xAppName}/instances/{xAppInstanceName}: get: summary: Returns the status of a given xapp tags: @@ -185,6 +158,62 @@ paths: description: Xapp not found '500': description: Internal error + /xapps/{xAppName}/instances/{xAppInstanceName}/start: + put: + summary: Start given xapp instance + tags: + - xapp + operationId: startXappInstanceByName + 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 + '400': + description: Invalid name supplied + '404': + description: Xapp not found + '500': + description: Internal error + /xapps/{xAppName}/instances/{xAppInstanceName}/stop: + put: + summary: Stop given xapp instance + tags: + - xapp + operationId: stopXappInstanceByName + 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 + '400': + description: Invalid name supplied + '404': + description: Xapp not found + '500': + description: Internal error /config: post: summary: Create xApp config @@ -205,7 +234,7 @@ paths: '201': description: xApp config successfully created schema: - $ref: '#/definitions/XAppConfig' + $ref: '#/definitions/ConfigValidationErrors' '400': description: Invalid input '422': @@ -231,7 +260,7 @@ paths: '200': description: xApp config successfully modified schema: - $ref: '#/definitions/XAppConfig' + $ref: '#/definitions/ConfigValidationErrors' '400': description: Invalid input '422': @@ -270,6 +299,27 @@ paths: description: Invalid parameters supplied '500': description: Internal error + /config/{configName}: + get: + summary: Returns the configuration of a single xapp + tags: + - xapp + operationId: getXappConfig + produces: + - application/json + parameters: + - name: configName + in: path + description: Name of xApp + required: true + type: string + responses: + '200': + description: successful query of xApp config + schema: + $ref: '#/definitions/XAppConfig' + '500': + description: Internal error /subscriptions: post: summary: Subscribe event @@ -308,7 +358,7 @@ paths: description: successful query of subscriptions schema: $ref: '#/definitions/allSubscriptions' - '/subscriptions/{subscriptionId}': + /subscriptions/{subscriptionId}: get: summary: Returns the information of subscription tags: @@ -384,7 +434,6 @@ definitions: type: array items: type: string - example: "xapp-dummy" AllDeployedXapps: type: array items: @@ -396,7 +445,6 @@ definitions: properties: name: type: string - example: xapp-dummy status: type: string description: xapp status in the RIC @@ -409,7 +457,6 @@ definitions: - deleting version: type: string - example: 1.2.3 instances: type: array items: @@ -421,7 +468,6 @@ definitions: properties: name: type: string - example: xapp-dummy-6cd577d9-4v255 status: type: string description: xapp instance status @@ -435,39 +481,70 @@ definitions: - 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 + XappDescriptor: + type: object + required: + - xappName + properties: + xappName: + type: string + description: Name of the xApp in helm chart + helmVersion: + type: string + description: The exact xapp helm chart version to install + releaseName: + type: string + description: Name of the xapp to be visible in Kubernetes + namespace: + type: string + description: Name of the namespace to which xApp is deployed. Overrides the value given in Helm chart value file. + overrideFile: + type: object + description: JSON string of override file for 'helm install' command + XappDescriptorList: + type: array + items: + $ref: '#/definitions/XappDescriptor' ConfigMetadata: type: object required: - name - - configName - - namespace properties: name: type: string description: Name of the xApp - example: xapp-dummy configName: type: string description: Name of the config map - example: xapp-dummy-config-map namespace: type: string description: Name of the namespace - example: ricxapp + ConfigValidationError: + type: object + required: + - field + - error + properties: + field: + type: string + description: Name of the parameter + error: + type: string + description: Description of validation error + ConfigValidationErrors: + type: array + items: + $ref: '#/definitions/ConfigValidationError' XAppConfig: type: object required: @@ -487,7 +564,18 @@ definitions: type: array items: $ref: '#/definitions/XAppConfig' - subscriptionRequest: + EventType: + type: string + description: Event which is subscribed + enum: + - deployed + - undeployed + - created + - modified + - deleted + - restarted + - all + SubscriptionData: type: object required: - targetUrl @@ -499,37 +587,29 @@ definitions: type: string example: 'http://localhost:11111/apps/webhook/' eventType: - type: string - description: Event which is subscribed - enum: - - created - - deleted - - all + $ref: '#/definitions/EventType' maxRetries: type: integer description: Maximum number of retries - example: 11 retryTimer: type: integer description: Time in seconds to wait before next retry - example: 22 + subscriptionRequest: + type: object + required: + - data + properties: + data: + $ref: '#/definitions/SubscriptionData' 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 - - updated - - all + $ref: '#/definitions/EventType' allSubscriptions: type: array items: @@ -539,41 +619,16 @@ definitions: 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 - - updated - - 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 + data: + $ref: '#/definitions/SubscriptionData' 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 - - updated + $ref: '#/definitions/EventType' xApps: $ref: '#/definitions/AllDeployedXapps' diff --git a/build/make.go.mk b/build/make.go.mk old mode 100644 new mode 100755 index 286d9b1..2208487 --- a/build/make.go.mk +++ b/build/make.go.mk @@ -58,13 +58,13 @@ GOMODFILES:=go.mod go.sum .SECONDEXPANSION: $(GO_CACHE_DIR)/%: $(GOFILES) $(GOMODFILES) $$(BUILDDEPS) @echo "Building:\t$*" - GO111MODULE=on GO_ENABLED=0 GOOS=linux $(GOBUILD) -o $@ ./$* + GO111MODULE=on GO_ENABLED=0 GOOS=linux $(GOBUILD) -o $@ cmd/appmgr.go .SECONDEXPANSION: $(GO_CACHE_DIR)/%_test: $(GOALLFILES) $(GOMODFILES) $$(BUILDDEPS) FORCE @echo "Testing:\t$*" - GO111MODULE=on GO_ENABLED=0 GOOS=linux $(GOTEST) -coverprofile $(COVEROUT) -c -o $@ ./$* + GO111MODULE=on GO_ENABLED=0 GOOS=linux $(GOTEST) -coverprofile $(COVEROUT) ./pkg/resthooks/ ./pkg/helm/ ./pkg/cm/ test -e $@ && (eval $(TESTENV) $@ -test.coverprofile $(COVEROUT) || false) || true test -e $@ && (go tool cover -html=$(COVEROUT) -o $(COVERHTML) || false) || true diff --git a/cmd/appmgr/main.go b/cmd/appmgr.go similarity index 83% rename from cmd/appmgr/main.go rename to cmd/appmgr.go index 8788405..b658367 100755 --- a/cmd/appmgr/main.go +++ b/cmd/appmgr.go @@ -19,15 +19,12 @@ package main -var Logger *Log +import ( + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/restful" +) func main() { - Logger = NewLogger("xapp-manager") - Logger.SetMdc("appmgr", "0.1.9") - loadConfig() - - m := XappManager{} - m.Initialize(&Helm{}, &ConfigMap{}) - - m.Run() + appmgr.Init() + restful.NewRestful().Run() } diff --git a/cmd/appmgr/api_test.go b/cmd/appmgr/api_test.go deleted file mode 100755 index 248ffee..0000000 --- a/cmd/appmgr/api_test.go +++ /dev/null @@ -1,313 +0,0 @@ -/* -================================================================================== - 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" - "encoding/json" - "errors" - "github.com/gorilla/mux" - "net/http" - "net/http/httptest" - "os" - "reflect" - "strconv" - "testing" - "time" -) - -var x XappManager -var xapp Xapp -var xapps []Xapp -var helmError error - -type MockedHelmer struct { -} - -func (h *MockedHelmer) SetCM(cm ConfigMapper) { -} - -func (sd *MockedHelmer) Initialize() { -} - -func (h *MockedHelmer) Status(name string) (Xapp, error) { - return xapp, helmError -} - -func (h *MockedHelmer) StatusAll() ([]Xapp, error) { - return xapps, helmError -} - -func (h *MockedHelmer) SearchAll() (s []string) { - return s -} - -func (h *MockedHelmer) List() (names []string, err error) { - return names, helmError -} - -func (h *MockedHelmer) Install(m XappDeploy) (Xapp, error) { - return xapp, helmError -} - -func (h *MockedHelmer) Delete(name string) (Xapp, error) { - return xapp, helmError -} - -// Test cases -func TestMain(m *testing.M) { - Logger = NewLogger("xapp-manager") - loadConfig() - - xapp = Xapp{} - xapps = []Xapp{} - - cm := MockedConfigMapper{} - h := MockedHelmer{} - x = XappManager{} - x.Initialize(&h, &cm) - - // Just run on the background (for coverage) - go x.Run() - x.ready = true - - time.Sleep(time.Duration(2 * time.Second)) - - code := m.Run() - os.Exit(code) -} - -func TestGetHealthCheck(t *testing.T) { - req, _ := http.NewRequest("GET", "/ric/v1/health/ready", 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) -} - -func TestGetConfigReturnsEmpty(t *testing.T) { - req, _ := http.NewRequest("GET", "/ric/v1/config", nil) - response := executeRequest(req) - - checkResponseCode(t, http.StatusOK, response.Code) -} - -func TestCreateConfigFailsWithMethodNotAllowed(t *testing.T) { - req, _ := http.NewRequest("POST", "/ric/v1/config", nil) - response := executeRequest(req) - - checkResponseCode(t, http.StatusMethodNotAllowed, response.Code) -} - -func TestCreateConfigOk(t *testing.T) { - payload := []byte(`{"name":"dummy-xapp"}`) - req, _ := http.NewRequest("POST", "/ric/v1/config", bytes.NewBuffer(payload)) - response := executeRequest(req) - - checkResponseCode(t, http.StatusCreated, response.Code) -} - -func TestDeleteConfigOk(t *testing.T) { - payload := []byte(`{"name":"dummy-xapp"}`) - req, _ := http.NewRequest("DELETE", "/ric/v1/config", bytes.NewBuffer(payload)) - response := executeRequest(req) - - checkResponseCode(t, http.StatusNoContent, 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) - var msgs MessageTypes - - instance := XappInstance{ - Name: iname, - Status: istatus, - Ip: ip, - Port: p, - TxMessages: msgs.TxMessages, - RxMessages: msgs.RxMessages, - } - x.Instances = append(x.Instances, instance) - - return -} diff --git a/cmd/appmgr/db.go b/cmd/appmgr/db.go deleted file mode 100755 index 97865c2..0000000 --- a/cmd/appmgr/db.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -================================================================================== - 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" - sdl "gerrit.oran-osc.org/r/ric-plt/sdlgo" - cmap "github.com/orcaman/concurrent-map" - "github.com/spf13/viper" - "time" -) - -type DB struct { - session *sdl.SdlInstance -} - -func (d *DB) Create() { - ns := viper.GetString("db.sessionNamespace") - d.session = sdl.NewSdlInstance(ns, sdl.NewDatabase()) - - // Test DB connection, and wait until ready! - for { - if _, err := d.session.GetAll(); err == nil { - return - } - Logger.Error("Database connection not ready, waiting ...") - time.Sleep(time.Duration(5 * time.Second)) - } -} - -func (d *DB) StoreSubscriptions(m cmap.ConcurrentMap) { - for v := range m.Iter() { - s := v.Val.(Subscription) - - data, err := json.Marshal(s.req) - if err != nil { - Logger.Error("json.marshal failed: %v ", err.Error()) - return - } - - if err := d.session.Set(s.req.Id, data); err != nil { - Logger.Error("DB.session.Set failed: %v ", err.Error()) - } - } -} - -func (d *DB) RestoreSubscriptions() (m cmap.ConcurrentMap) { - m = cmap.New() - - keys, err := d.session.GetAll() - if err != nil { - Logger.Error("DB.session.GetAll failed: %v ", err.Error()) - return - } - - for _, key := range keys { - value, err := d.session.Get([]string{key}) - if err != nil { - Logger.Error("DB.session.Get failed: %v ", err.Error()) - return - } - - var item SubscriptionReq - if err = json.Unmarshal([]byte(value[key].(string)), &item); err != nil { - Logger.Error("json.Unmarshal failed: %v ", err.Error()) - return - } - - resp := SubscriptionResp{key, 0, item.EventType} - m.Set(key, Subscription{item, resp}) - } - - return m -} diff --git a/cmd/appmgr/subscriptions.go b/cmd/appmgr/subscriptions.go deleted file mode 100755 index 53bc212..0000000 --- a/cmd/appmgr/subscriptions.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -================================================================================== - 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" - "encoding/json" - "github.com/segmentio/ksuid" - "net/http" - "time" -) - -func (sd *SubscriptionDispatcher) Initialize() { - sd.client = &http.Client{} - - sd.db = &DB{} - sd.db.Create() - sd.subscriptions = sd.db.RestoreSubscriptions() -} - -func (sd *SubscriptionDispatcher) Add(sr SubscriptionReq) (resp SubscriptionResp) { - // Skip duplicates - for v := range sd.subscriptions.IterBuffered() { - r := v.Val.(Subscription).req - if r.TargetUrl == sr.TargetUrl && r.EventType == sr.EventType { - Logger.Info("Similar subscription already exists!") - return - } - } - - key := ksuid.New().String() - resp = SubscriptionResp{key, 0, sr.EventType} - sr.Id = key - - sd.subscriptions.Set(key, Subscription{sr, resp}) - sd.db.StoreSubscriptions(sd.subscriptions) - - Logger.Info("Sub: 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 { - Logger.Info("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 { - Logger.Info("Subscription id=%s found: %v ... deleting", id, v.(Subscription).req) - - sd.subscriptions.Remove(id) - sd.db.StoreSubscriptions(sd.subscriptions) - - 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 { - Logger.Info("Subscription id=%s found: %v ... updating", id, s.(Subscription).req) - - sr.Id = id - sd.subscriptions.Set(id, Subscription{sr, s.(Subscription).resp}) - sd.db.StoreSubscriptions(sd.subscriptions) - - return sr, found - } - return SubscriptionReq{}, false -} - -func (sd *SubscriptionDispatcher) Publish(x Xapp, et EventType) { - sd.notifyClients([]Xapp{x}, et) -} - -func (sd *SubscriptionDispatcher) notifyClients(xapps []Xapp, et EventType) { - if len(xapps) == 0 || len(sd.subscriptions) == 0 { - Logger.Info("Nothing to publish [%d:%d]", len(xapps), len(sd.subscriptions)) - return - } - - sd.Seq = sd.Seq + 1 - for v := range sd.subscriptions.Iter() { - go sd.notify(xapps, et, v.Val.(Subscription), sd.Seq) - } -} - -func (sd *SubscriptionDispatcher) notify(xapps []Xapp, et EventType, s Subscription, seq int) error { - xappData, err := json.Marshal(xapps) - if err != nil { - Logger.Info("json.Marshal failed: %v", err) - return err - } - - notif := SubscriptionNotif{Id: s.req.Id, Version: seq, EventType: string(et), XApps: string(xappData)} - jsonData, err := json.Marshal(notif) - if err != nil { - Logger.Info("json.Marshal failed: %v", err) - return err - } - - // Execute the request with retry policy - return sd.retry(s, func() error { - Logger.Info("Posting notification to targetUrl=%s: %v", s.req.TargetUrl, notif) - resp, err := http.Post(s.req.TargetUrl, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - Logger.Info("Posting to subscription failed: %v", err) - return err - } - - if resp.StatusCode != http.StatusOK { - Logger.Info("Client returned error code: %d", resp.StatusCode) - return err - } - - Logger.Info("subscription to '%s' dispatched, response code: %d", 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/cmd/appmgr/subscriptions_test.go b/cmd/appmgr/subscriptions_test.go deleted file mode 100755 index 5d79d00..0000000 --- a/cmd/appmgr/subscriptions_test.go +++ /dev/null @@ -1,198 +0,0 @@ -/* -================================================================================== - 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" - "encoding/json" - "fmt" - sdl "gerrit.oran-osc.org/r/ric-plt/sdlgo" - "github.com/spf13/viper" - "log" - "net" - "net/http" - "net/http/httptest" - "testing" -) - -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 TestTeardown(t *testing.T) { - db := sdl.NewSdlInstance(viper.GetString("db.sessionNamespace"), sdl.NewDatabase()) - db.RemoveAll() -} - -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) - } -} diff --git a/cmd/appmgr/types.go b/cmd/appmgr/types.go deleted file mode 100755 index 1c9556d..0000000 --- a/cmd/appmgr/types.go +++ /dev/null @@ -1,159 +0,0 @@ -/* -================================================================================== - 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 XappDeploy struct { - Name string `json:"name"` - ConfigName string `json:"configName, omitempty"` - Namespace string `json:"namespace, omitempty"` - ServiceName string `json:"serviceName, omitempty"` - ImageRepo string `json:"imageRepo, omitempty"` - Hostname string `json:"hostname, omitempty"` -} - -type XappManager struct { - router *mux.Router - helm Helmer - cm ConfigMapper - sd SubscriptionDispatcher - opts CmdOptions - ready bool -} - -type ConfigMapper interface { - UploadConfig() (cfg []XAppConfig) - GetConfigMap(m XappDeploy, c *interface{}) (err error) - CreateConfigMap(r XAppConfig) (errList []CMError, err error) - UpdateConfigMap(r XAppConfig) (errList []CMError, err error) - DeleteConfigMap(r XAppConfig) (cm interface{}, err error) - ReadSchema(name string, c *XAppConfig) (err error) - PurgeConfigMap(m XappDeploy) (cm interface{}, err error) - RestoreConfigMap(m XappDeploy, cm interface{}) (err error) - ReadConfigMap(name string, ns string, c *interface{}) (err error) - ApplyConfigMap(r XAppConfig, action string) (err error) - GetMessages(name string) (msgs MessageTypes) - GetNamespace(ns string) string - GetNamesFromHelmRepo() (names []string) -} - -type Helmer interface { - SetCM(ConfigMapper) - Initialize() - Install(m XappDeploy) (xapp Xapp, err error) - Status(name string) (xapp Xapp, err error) - StatusAll() (xapps []Xapp, err error) - SearchAll() (xapps []string) - List() (xapps []string, err error) - Delete(name string) (xapp Xapp, err error) -} - -type Helm struct { - host string - chartPath string - initDone bool - cm ConfigMapper -} - -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"` - XApps string `json:"xApps"` -} - -type Subscription struct { - req SubscriptionReq - resp SubscriptionResp -} - -type SubscriptionDispatcher struct { - client *http.Client - subscriptions cmap.ConcurrentMap - db *DB - Seq int -} - -type MessageTypes struct { - TxMessages []string `json:"txMessages"` - RxMessages []string `json:"rxMessages"` -} - -type EventType string - -const ( - Created EventType = "created" - Updated EventType = "updated" - Deleted EventType = "deleted" -) - -const ( - MdclogErr = 1 //! Error level log entry - MdclogWarn = 2 //! Warning level log entry - MdclogInfo = 3 //! Info level log entry - MdclogDebug = 4 //! Debug level log entry -) diff --git a/container-tag.yaml b/container-tag.yaml old mode 100644 new mode 100755 index a704888..3724441 --- a/container-tag.yaml +++ b/container-tag.yaml @@ -1,4 +1,4 @@ # The Jenkins job uses this string for the tag in the image name # for example nexus3.o-ran-sc.org:10004/my-image-name:my-tag --- -tag: '0.1.10' +tag: '0.2.0' diff --git a/go.mod b/go.mod index 6a22825..253e4a2 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,25 @@ require ( gerrit.oran-osc.org/r/ric-plt/sdlgo v0.0.0 github.com/BurntSushi/toml v0.3.1 // indirect github.com/fsnotify/fsnotify v1.4.7 + github.com/ghodss/yaml v1.0.0 + github.com/go-openapi/errors v0.19.2 + github.com/go-openapi/loads v0.19.4 + github.com/go-openapi/runtime v0.19.7 + github.com/go-openapi/spec v0.19.4 + github.com/go-openapi/strfmt v0.19.3 + github.com/go-openapi/swag v0.19.5 + github.com/go-openapi/validate v0.19.4 github.com/gorilla/mux v1.7.1 + github.com/jessevdk/go-flags v1.4.0 github.com/orcaman/concurrent-map v0.0.0-20190314100340-2693aad1ed75 github.com/segmentio/ksuid v1.0.2 github.com/spf13/viper v1.3.2 + github.com/stretchr/testify v1.4.0 github.com/valyala/fastjson v1.4.1 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.1.0 + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 ) replace gerrit.oran-osc.org/r/ric-plt/sdlgo => gerrit.oran-osc.org/r/ric-plt/sdlgo.git v0.0.1 diff --git a/go.sum b/go.sum index e0539a4..3ffdc4b 100644 --- a/go.sum +++ b/go.sum @@ -5,27 +5,112 @@ gerrit.oran-osc.org/r/ric-plt/sdlgo.git v0.0.1 h1:l2dl31r++3xhgCumTzwvuo0/F415eq gerrit.oran-osc.org/r/ric-plt/sdlgo.git v0.0.1/go.mod h1:LVhkNS82IofJTBK/VYPKiYed9MG/3OFwvWC6MGSDw1w= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5 h1:8b2ZgKfKIUTVQpTb77MoRDIMEIwvDVw40o3aOXdfYzI= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.4 h1:5I4CCSqoWzT+82bBkNIvmLc0UOsoKKQ4Fz+3VxOB7SY= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.7 h1:b2zcE9GCjDVtguugU7+S95vkHjwQEjz/lB+8LOuA9Nw= +github.com/go-openapi/runtime v0.19.7/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3 h1:eRfyY5SkaNJCAwmmMcADjY31ow9+N7MCLW7oRkbsINA= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.4 h1:LGjO87VyXY3bIKjlYpXSFuLRG2mTeuYlZyeNwFFWpyM= +github.com/go-openapi/validate v0.19.4/go.mod h1:BkJ0ZmXui7yB0bJXWSXgLPNTmbLVeX/3D1xn/N9mMUM= github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -35,6 +120,7 @@ github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/orcaman/concurrent-map v0.0.0-20190314100340-2693aad1ed75 h1:IV56VwUb9Ludyr7s53CMuEh4DdTnnQtEPLEgLyJ0kHI= github.com/orcaman/concurrent-map v0.0.0-20190314100340-2693aad1ed75/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -53,9 +139,15 @@ github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/valyala/fastjson v1.4.1 h1:hrltpHpIpkaxll8QltMU8c3QZ5+qIiCL8yKqPFJI/yE= github.com/valyala/fastjson v1.4.1/go.mod h1:nV6MsjxL2IMJQUoHDIrjEI7oLyeqK6aBD7EFWPsvP8o= @@ -66,18 +158,44 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1 h1:Sq1fR+0c58RME5EoqKdjkiQAmPjmfHlZOoRI6fTUOcs= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -85,3 +203,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/cmd/appmgr/config.go b/pkg/appmgr/appmgr.go similarity index 75% rename from cmd/appmgr/config.go rename to pkg/appmgr/appmgr.go index 5e1975e..28e98f9 100755 --- a/cmd/appmgr/config.go +++ b/pkg/appmgr/appmgr.go @@ -17,16 +17,27 @@ ================================================================================== */ -package main +package appmgr import ( "flag" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/logger" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" "log" + "net/http" ) -const DEFAULT_CONFIG_FILE = "config/appmgr.yaml" +const DEFAULT_CONFIG_FILE = "../../config/appmgr.yaml" + +var Logger *logger.Log + +func LogRestRequests(inner http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + inner.ServeHTTP(w, r) + Logger.Info("Logger: method=%s url=%s", r.Method, r.URL.RequestURI()) + }) +} func parseCmd() string { var fileName *string @@ -36,9 +47,15 @@ func parseCmd() string { return *fileName } +func watch() { + viper.WatchConfig() + viper.OnConfigChange(func(e fsnotify.Event) { + log.Println("config file changed ", e.Name) + }) +} + func loadConfig() { viper.SetConfigFile(parseCmd()) - if err := viper.ReadInConfig(); err != nil { log.Fatalf("Error reading config file, %s", err) } @@ -48,9 +65,8 @@ func loadConfig() { watch() } -func watch() { - viper.WatchConfig() - viper.OnConfigChange(func(e fsnotify.Event) { - log.Println("config file changed ", e.Name) - }) +func Init() { + loadConfig() + Logger = logger.NewLogger("appmgr") + Logger.SetMdc("xm", "0.2.0") } diff --git a/pkg/appmgr/types.go b/pkg/appmgr/types.go new file mode 100755 index 0000000..afdb0b4 --- /dev/null +++ b/pkg/appmgr/types.go @@ -0,0 +1,90 @@ +/* +================================================================================== + 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 appmgr + +import ( + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" +) + +type ConfigMap struct { + Kind string `json:"kind"` + ApiVersion string `json:"apiVersion"` + Data interface{} `json:"data"` + Metadata CMMetadata `json:"metadata"` +} + +type CMMetadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +type ConfigMapper interface { + UploadConfig() (cfg []models.XAppConfig) + GetConfigMap(m models.XappDescriptor, c *interface{}) (err error) + CreateConfigMap(r models.XAppConfig) (errList models.ConfigValidationErrors, err error) + UpdateConfigMap(r models.XAppConfig) (errList models.ConfigValidationErrors, err error) + DeleteConfigMap(r models.XAppConfig) (cm interface{}, err error) + ReadSchema(name string, c *models.XAppConfig) (err error) + PurgeConfigMap(m models.XappDescriptor) (cm interface{}, err error) + RestoreConfigMap(m models.XappDescriptor, cm interface{}) (err error) + ReadConfigMap(name string, ns string, c *interface{}) (err error) + ApplyConfigMap(r models.XAppConfig, action string) (err error) + GetMessages(name string) (msgs MessageTypes) + GetNamespace(ns string) string + GetNamesFromHelmRepo() (names []string) +} + +type Helmer interface { + SetCM(ConfigMapper) + Initialize() + Install(m models.XappDescriptor) (xapp models.Xapp, err error) + Status(name string) (xapp models.Xapp, err error) + StatusAll() (xapps models.AllDeployedXapps, err error) + SearchAll() (xapps []string) + List() (xapps []string, err error) + Delete(name string) (xapp models.Xapp, err error) +} + +type Helm struct { + host string + chartPath string + initDone bool + cm ConfigMapper +} + +type MessageTypes struct { + TxMessages []string `json:"txMessages"` + RxMessages []string `json:"rxMessages"` +} + +type EventType string + +const ( + Created EventType = "created" + Updated EventType = "updated" + Deleted EventType = "deleted" +) + +const ( + MdclogErr = 1 //! Error level log entry + MdclogWarn = 2 //! Warning level log entry + MdclogInfo = 3 //! Info level log entry + MdclogDebug = 4 //! Debug level log entry +) diff --git a/cmd/appmgr/desc.go b/pkg/cm/cm.go similarity index 51% rename from cmd/appmgr/desc.go rename to pkg/cm/cm.go index 8cc39a5..af5372d 100755 --- a/cmd/appmgr/desc.go +++ b/pkg/cm/cm.go @@ -17,7 +17,7 @@ ================================================================================== */ -package main +package cm import ( "encoding/json" @@ -27,52 +27,32 @@ import ( "github.com/valyala/fastjson" "github.com/xeipuuv/gojsonschema" "io/ioutil" - "log" "os" "path" "regexp" "strings" "time" -) - -type ConfigMetadata struct { - Name string `json:"name"` - ConfigName string `json:"configName, omitempty"` - Namespace string `json:"namespace, omitempty"` -} -type XAppConfig struct { - Metadata ConfigMetadata `json:"metadata"` - Descriptor interface{} `json:"descriptor, omitempty"` - Configuration interface{} `json:"config, omitempty"` -} - -type ConfigMap struct { - Kind string `json:"kind"` - ApiVersion string `json:"apiVersion"` - Data interface{} `json:"data"` - Metadata CMMetadata `json:"metadata"` -} + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/util" +) -type CMMetadata struct { - Name string `json:"name"` - Namespace string `json:"namespace"` -} +type CM struct{} -type CMError struct { - Field string `json:"field"` - Description string `json:"description"` +func NewCM() *CM { + return &CM{} } -func (cm *ConfigMap) UploadConfig() (cfg []XAppConfig) { +func (cm *CM) UploadConfig() (cfg models.AllXappConfig) { ns := cm.GetNamespace("") for _, name := range cm.GetNamesFromHelmRepo() { if name == "appmgr" { continue } - c := XAppConfig{ - Metadata: ConfigMetadata{Name: name, Namespace: ns, ConfigName: cm.GetConfigMapName(name, ns)}, + c := models.XAppConfig{ + Metadata: &models.ConfigMetadata{Name: &name, Namespace: ns, ConfigName: cm.GetConfigMapName(name, ns)}, } err := cm.ReadSchema(name, &c) @@ -80,17 +60,17 @@ func (cm *ConfigMap) UploadConfig() (cfg []XAppConfig) { continue } - err = cm.ReadConfigMap(c.Metadata.ConfigName, ns, &c.Configuration) + err = cm.ReadConfigMap(c.Metadata.ConfigName, ns, &c.Config) if err != nil { - log.Println("No active configMap found, using default!") + appmgr.Logger.Info("No active configMap found, using default!") } - cfg = append(cfg, c) + cfg = append(cfg, &c) } return } -func (cm *ConfigMap) ReadSchema(name string, c *XAppConfig) (err error) { +func (cm *CM) ReadSchema(name string, c *models.XAppConfig) (err error) { if err = cm.FetchChart(name); err != nil { return } @@ -101,21 +81,21 @@ func (cm *ConfigMap) ReadSchema(name string, c *XAppConfig) (err error) { return } - err = cm.ReadFile(path.Join(tarDir, name, viper.GetString("xapp.config")), &c.Configuration) + err = cm.ReadFile(path.Join(tarDir, name, viper.GetString("xapp.config")), &c.Config) if err != nil { return } if err = os.RemoveAll(path.Join(tarDir, name)); err != nil { - log.Println("RemoveAll failed", err) + appmgr.Logger.Info("RemoveAll failed: %v", err) } return } -func (cm *ConfigMap) ReadConfigMap(ConfigName string, ns string, c *interface{}) (err error) { +func (cm *CM) ReadConfigMap(ConfigName string, ns string, c *interface{}) (err error) { args := fmt.Sprintf("get configmap -o jsonpath='{.data.config-file\\.json}' -n %s %s", ns, ConfigName) - configMapJson, err := KubectlExec(args) + configMapJson, err := util.KubectlExec(args) if err != nil { return } @@ -128,46 +108,43 @@ func (cm *ConfigMap) ReadConfigMap(ConfigName string, ns string, c *interface{}) return } -func (cm *ConfigMap) ApplyConfigMap(r XAppConfig, action string) (err error) { - c := ConfigMap{ +func (cm *CM) ApplyConfigMap(r models.XAppConfig, action string) (err error) { + c := appmgr.ConfigMap{ Kind: "ConfigMap", ApiVersion: "v1", - Metadata: CMMetadata{Name: r.Metadata.Name, Namespace: r.Metadata.Namespace}, - Data: r.Configuration, + Metadata: appmgr.CMMetadata{Name: *r.Metadata.Name, Namespace: r.Metadata.Namespace}, + Data: r.Config, } cmJson, err := json.Marshal(c.Data) if err != nil { - log.Println("Config marshalling failed: ", err) + appmgr.Logger.Info("Config marshalling failed: %v", err) return } cmFile := viper.GetString("xapp.tmpConfig") err = ioutil.WriteFile(cmFile, cmJson, 0644) if err != nil { - log.Println("WriteFile failed: ", err) + appmgr.Logger.Info("WriteFile failed: %v", err) return } cmd := " create configmap -n %s %s --from-file=%s -o json --dry-run | kubectl %s -f -" args := fmt.Sprintf(cmd, r.Metadata.Namespace, r.Metadata.ConfigName, cmFile, action) - _, err = KubectlExec(args) + _, err = util.KubectlExec(args) if err != nil { return } - log.Println("Configmap changes done!") + appmgr.Logger.Info("Configmap changes done!") return } -func (cm *ConfigMap) GetConfigMap(m XappDeploy, c *interface{}) (err error) { - if m.ConfigName == "" { - m.ConfigName = cm.GetConfigMapName(m.Name, m.Namespace) - } - return cm.ReadConfigMap(m.ConfigName, m.Namespace, c) +func (cm *CM) GetConfigMap(m models.XappDescriptor, c *interface{}) (err error) { + return cm.ReadConfigMap(cm.GetConfigMapName(*m.XappName, m.Namespace), m.Namespace, c) } -func (cm *ConfigMap) CreateConfigMap(r XAppConfig) (errList []CMError, err error) { +func (cm *CM) CreateConfigMap(r models.XAppConfig) (errList models.ConfigValidationErrors, err error) { if errList, err = cm.Validate(r); err != nil { return } @@ -175,7 +152,7 @@ func (cm *ConfigMap) CreateConfigMap(r XAppConfig) (errList []CMError, err error return } -func (cm *ConfigMap) UpdateConfigMap(r XAppConfig) (errList []CMError, err error) { +func (cm *CM) UpdateConfigMap(r models.XAppConfig) (errList models.ConfigValidationErrors, err error) { if errList, err = cm.Validate(r); err != nil { return } @@ -185,39 +162,33 @@ func (cm *ConfigMap) UpdateConfigMap(r XAppConfig) (errList []CMError, err error return } -func (cm *ConfigMap) DeleteConfigMap(r XAppConfig) (c interface{}, err error) { - err = cm.ReadConfigMap(r.Metadata.ConfigName, r.Metadata.Namespace, &c) +func (cm *CM) DeleteConfigMap(r models.ConfigMetadata) (c interface{}, err error) { + err = cm.ReadConfigMap(r.ConfigName, r.Namespace, &c) if err == nil { - args := fmt.Sprintf(" delete configmap --namespace=%s %s", r.Metadata.Namespace, r.Metadata.ConfigName) - _, err = KubectlExec(args) + args := fmt.Sprintf(" delete configmap --namespace=%s %s", r.Namespace, r.ConfigName) + _, err = util.KubectlExec(args) } return } -func (cm *ConfigMap) PurgeConfigMap(m XappDeploy) (c interface{}, err error) { - if m.ConfigName == "" { - m.ConfigName = cm.GetConfigMapName(m.Name, m.Namespace) - } - md := ConfigMetadata{Name: m.Name, Namespace: m.Namespace, ConfigName: m.ConfigName} +func (cm *CM) PurgeConfigMap(m models.XappDescriptor) (c interface{}, err error) { + md := models.ConfigMetadata{Name: m.XappName, Namespace: m.Namespace, ConfigName: cm.GetConfigMapName(*m.XappName, m.Namespace)} - return cm.DeleteConfigMap(XAppConfig{Metadata: md}) + return cm.DeleteConfigMap(md) } -func (cm *ConfigMap) RestoreConfigMap(m XappDeploy, c interface{}) (err error) { - if m.ConfigName == "" { - m.ConfigName = cm.GetConfigMapName(m.Name, m.Namespace) - } - md := ConfigMetadata{Name: m.Name, Namespace: m.Namespace, ConfigName: m.ConfigName} +func (cm *CM) RestoreConfigMap(m models.XappDescriptor, c interface{}) (err error) { + md := &models.ConfigMetadata{Name: m.XappName, Namespace: m.Namespace, ConfigName: cm.GetConfigMapName(*m.XappName, m.Namespace)} time.Sleep(time.Duration(10 * time.Second)) - return cm.ApplyConfigMap(XAppConfig{Metadata: md, Configuration: c}, "create") + return cm.ApplyConfigMap(models.XAppConfig{Metadata: md, Config: c}, "create") } -func (cm *ConfigMap) GetNamesFromHelmRepo() (names []string) { +func (cm *CM) GetNamesFromHelmRepo() (names []string) { rname := viper.GetString("helm.repo-name") cmdArgs := strings.Join([]string{"search ", rname}, "") - out, err := HelmExec(cmdArgs) + out, err := util.HelmExec(cmdArgs) if err != nil { return } @@ -234,67 +205,69 @@ func (cm *ConfigMap) GetNamesFromHelmRepo() (names []string) { return names } -func (cm *ConfigMap) Validate(req XAppConfig) (errList []CMError, err error) { - c := XAppConfig{} - err = cm.ReadSchema(req.Metadata.Name, &c) +func (cm *CM) Validate(req models.XAppConfig) (errList models.ConfigValidationErrors, err error) { + c := models.XAppConfig{} + err = cm.ReadSchema(*req.Metadata.Name, &c) if err != nil { - log.Printf("No schema file found for '%s', aborting ...", req.Metadata.Name) + appmgr.Logger.Info("No schema file found for '%s', aborting ...", *req.Metadata.Name) return } - return cm.doValidate(c.Descriptor, req.Configuration) + return cm.doValidate(c.Descriptor, req.Config) } -func (cm *ConfigMap) doValidate(schema, cfg interface{}) (errList []CMError, err error) { +func (cm *CM) doValidate(schema, cfg interface{}) (errList models.ConfigValidationErrors, err error) { schemaLoader := gojsonschema.NewGoLoader(schema) documentLoader := gojsonschema.NewGoLoader(cfg) result, err := gojsonschema.Validate(schemaLoader, documentLoader) if err != nil { - log.Println("Validation failed: ", err) + appmgr.Logger.Info("Validation failed: %v", err) return } if result.Valid() == false { - log.Println("The document is not valid, Errors: ", result.Errors()) + appmgr.Logger.Info("The document is not valid, Errors: %v", result.Errors()) for _, desc := range result.Errors() { - errList = append(errList, CMError{Field: desc.Field(), Description: desc.Description()}) + field := desc.Field() + validationError := desc.Description() + errList = append(errList, &models.ConfigValidationError{Field: &field, Error: &validationError}) } return errList, errors.New("Validation failed!") } return } -func (cm *ConfigMap) ReadFile(name string, data interface{}) (err error) { +func (cm *CM) ReadFile(name string, data interface{}) (err error) { f, err := ioutil.ReadFile(name) if err != nil { - log.Printf("Reading '%s' file failed: %v", name, err) + appmgr.Logger.Info("Reading '%s' file failed: %v", name, err) return } err = json.Unmarshal(f, &data) if err != nil { - log.Printf("Unmarshalling '%s' file failed: %v", name, err) + appmgr.Logger.Info("Unmarshalling '%s' file failed: %v", name, err) return } return } -func (cm *ConfigMap) FetchChart(name string) (err error) { +func (cm *CM) FetchChart(name string) (err error) { tarDir := viper.GetString("xapp.tarDir") repo := viper.GetString("helm.repo-name") fetchArgs := fmt.Sprintf("--untar --untardir %s %s/%s", tarDir, repo, name) - _, err = HelmExec(strings.Join([]string{"fetch ", fetchArgs}, "")) + _, err = util.HelmExec(strings.Join([]string{"fetch ", fetchArgs}, "")) return } -func (cm *ConfigMap) GetMessages(name string) (msgs MessageTypes) { - log.Println("Fetching tx/rx messages for: ", name) +func (cm *CM) GetMessages(name string) (msgs appmgr.MessageTypes) { + appmgr.Logger.Info("Fetching tx/rx messages for: %s", name) ns := cm.GetNamespace("") args := fmt.Sprintf("get configmap -o jsonpath='{.data.config-file\\.json}' -n %s %s", ns, cm.GetConfigMapName(name, ns)) - out, err := KubectlExec(args) + out, err := util.KubectlExec(args) if err != nil { return } @@ -302,7 +275,7 @@ func (cm *ConfigMap) GetMessages(name string) (msgs MessageTypes) { var p fastjson.Parser v, err := p.Parse(string(out)) if err != nil { - log.Printf("fastjson.Parser for '%s' failed: %v", name, err) + appmgr.Logger.Info("fastjson.Parser for '%s' failed: %v", name, err) return } @@ -316,11 +289,11 @@ func (cm *ConfigMap) GetMessages(name string) (msgs MessageTypes) { return } -func (cm *ConfigMap) GetConfigMapName(xappName, namespace string) string { +func (cm *CM) GetConfigMapName(xappName, namespace string) string { return " configmap-" + namespace + "-" + xappName + "-appconfig" } -func (cm *ConfigMap) GetNamespace(ns string) string { +func (cm *CM) GetNamespace(ns string) string { if ns != "" { return ns } diff --git a/cmd/appmgr/desc_test.go b/pkg/cm/cm_test.go similarity index 59% rename from cmd/appmgr/desc_test.go rename to pkg/cm/cm_test.go index 8f005a1..be600bb 100755 --- a/cmd/appmgr/desc_test.go +++ b/pkg/cm/cm_test.go @@ -17,14 +17,18 @@ ================================================================================== */ -package main +package cm import ( "encoding/json" "errors" - "log" + "os" "reflect" "testing" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/util" ) var helmSearchOutput = ` @@ -66,35 +70,35 @@ type ConfigSample struct { type MockedConfigMapper struct { } -func (cm *MockedConfigMapper) ReadSchema(name string, c *XAppConfig) (err error) { +func (cm *MockedConfigMapper) ReadSchema(name string, c *models.XAppConfig) (err error) { return } -func (cm *MockedConfigMapper) UploadConfig() (cfg []XAppConfig) { +func (cm *MockedConfigMapper) UploadConfig() (cfg []models.XAppConfig) { return } -func (cm *MockedConfigMapper) CreateConfigMap(r XAppConfig) (errList []CMError, err error) { +func (cm *MockedConfigMapper) CreateConfigMap(r models.XAppConfig) (errList models.ConfigValidationErrors, err error) { return } -func (cm *MockedConfigMapper) GetConfigMap(m XappDeploy, c *interface{}) (err error) { +func (cm *MockedConfigMapper) GetConfigMap(m models.XappDescriptor, c *interface{}) (err error) { return } -func (cm *MockedConfigMapper) UpdateConfigMap(r XAppConfig) (errList []CMError, err error) { +func (cm *MockedConfigMapper) UpdateConfigMap(r models.XAppConfig) (errList models.ConfigValidationErrors, err error) { return } -func (cm *MockedConfigMapper) DeleteConfigMap(r XAppConfig) (c interface{}, err error) { +func (cm *MockedConfigMapper) DeleteConfigMap(r models.XAppConfig) (c interface{}, err error) { return } -func (cm *MockedConfigMapper) PurgeConfigMap(m XappDeploy) (c interface{}, err error) { +func (cm *MockedConfigMapper) PurgeConfigMap(m models.XappDescriptor) (c interface{}, err error) { return } -func (cm *MockedConfigMapper) RestoreConfigMap(m XappDeploy, c interface{}) (err error) { +func (cm *MockedConfigMapper) RestoreConfigMap(m models.XappDescriptor, c interface{}) (err error) { return } @@ -102,7 +106,7 @@ func (cm *MockedConfigMapper) ReadConfigMap(name string, ns string, c *interface return } -func (cm *MockedConfigMapper) ApplyConfigMap(r XAppConfig, action string) (err error) { +func (cm *MockedConfigMapper) ApplyConfigMap(r models.XAppConfig, action string) (err error) { return } @@ -110,7 +114,7 @@ func (cm *MockedConfigMapper) FetchChart(name string) (err error) { return } -func (cm *MockedConfigMapper) GetMessages(name string) (msgs MessageTypes) { +func (cm *MockedConfigMapper) GetMessages(name string) (msgs appmgr.MessageTypes) { return } @@ -123,200 +127,186 @@ func (cm *MockedConfigMapper) GetNamesFromHelmRepo() (names []string) { } // Test cases +func TestMain(m *testing.M) { + appmgr.Init() + appmgr.Logger.SetLevel(0) + + code := m.Run() + os.Exit(code) +} + func TestGetMessages(t *testing.T) { - cm := ConfigMap{} - expectedMsgs := MessageTypes{ + expectedMsgs := appmgr.MessageTypes{ TxMessages: []string{"RIC_X2_LOAD_INFORMATION"}, RxMessages: []string{"RIC_X2_LOAD_INFORMATION"}, } - KubectlExec = func(args string) (out []byte, err error) { + util.KubectlExec = func(args string) (out []byte, err error) { return []byte(kubectlConfigmapOutput), nil } - result := cm.GetMessages("dummy-xapp") + result := NewCM().GetMessages("dummy-xapp") if !reflect.DeepEqual(result, expectedMsgs) { t.Errorf("TestGetMessages failed: expected: %v, got: %v", expectedMsgs, result) } } func TestHelmNamespace(t *testing.T) { - cm := ConfigMap{} - - if cm.GetNamespace("pltxapp") != "pltxapp" { + if NewCM().GetNamespace("pltxapp") != "pltxapp" { t.Errorf("getNamespace failed!") } - if cm.GetNamespace("") != "default" { + if NewCM().GetNamespace("") != "ricxapp" { t.Errorf("getNamespace failed!") } } func TestFetchChartFails(t *testing.T) { - cm := ConfigMap{} - - if cm.FetchChart("dummy-xapp") == nil { + if NewCM().FetchChart("dummy-xapp") == nil { t.Errorf("TestFetchChart failed!") } } func TestFetchChartSuccess(t *testing.T) { - cm := ConfigMap{} - - HelmExec = func(args string) (out []byte, err error) { + util.HelmExec = func(args string) (out []byte, err error) { return } - if cm.FetchChart("dummy-xapp") != nil { + if NewCM().FetchChart("dummy-xapp") != nil { t.Errorf("TestFetchChart failed!") } } func TestGetNamesFromHelmRepoSuccess(t *testing.T) { - cm := ConfigMap{} expectedResult := []string{"anr", "appmgr", "dualco", "reporter", "uemgr"} - HelmExec = func(args string) (out []byte, err error) { + util.HelmExec = func(args string) (out []byte, err error) { return []byte(helmSearchOutput), nil } - names := cm.GetNamesFromHelmRepo() + names := NewCM().GetNamesFromHelmRepo() if !reflect.DeepEqual(names, expectedResult) { t.Errorf("GetNamesFromHelmRepo failed: expected %v, got %v", expectedResult, names) } } func TestGetNamesFromHelmRepoFailure(t *testing.T) { - cm := ConfigMap{} expectedResult := []string{} - HelmExec = func(args string) (out []byte, err error) { + util.HelmExec = func(args string) (out []byte, err error) { return []byte(helmSearchOutput), errors.New("Command failed!") } - names := cm.GetNamesFromHelmRepo() + names := NewCM().GetNamesFromHelmRepo() if names != nil { t.Errorf("GetNamesFromHelmRepo failed: expected %v, got %v", expectedResult, names) } } func TestApplyConfigMapSuccess(t *testing.T) { - cm := ConfigMap{} - m := ConfigMetadata{Name: "dummy-xapp", Namespace: "ricxapp"} + name := "dummy-xapp" + m := models.ConfigMetadata{Name: &name, Namespace: "ricxapp"} s := ConfigSample{5, "localhost"} - KubectlExec = func(args string) (out []byte, err error) { - log.Println("TestApplyConfigMapSuccess: ", args) + util.KubectlExec = func(args string) (out []byte, err error) { return []byte(`{"logger": {"level": 2}}`), nil } - err := cm.ApplyConfigMap(XAppConfig{Metadata: m, Configuration: s}, "create") + err := NewCM().ApplyConfigMap(models.XAppConfig{Metadata: &m, Config: s}, "create") if err != nil { t.Errorf("ApplyConfigMap failed: %v", err) } } func TestRestoreConfigMapSuccess(t *testing.T) { - cm := ConfigMap{} - m := XappDeploy{Name: "dummy-xapp", Namespace: "ricxapp"} + name := "dummy-xapp" + m := models.XappDescriptor{XappName: &name, Namespace: "ricxapp"} s := ConfigSample{5, "localhost"} - KubectlExec = func(args string) (out []byte, err error) { - log.Println("TestRestoreConfigMapSuccess: ", args) + util.KubectlExec = func(args string) (out []byte, err error) { return []byte(`{"logger": {"level": 2}}`), nil } - err := cm.RestoreConfigMap(m, s) + err := NewCM().RestoreConfigMap(m, s) if err != nil { t.Errorf("RestoreConfigMap failed: %v", err) } } func TestDeleteConfigMapSuccess(t *testing.T) { - cm := ConfigMap{} - - HelmExec = func(args string) (out []byte, err error) { + util.HelmExec = func(args string) (out []byte, err error) { return []byte("ok"), nil } - KubectlExec = func(args string) (out []byte, err error) { - log.Println("TestDeleteConfigMapSuccess: ", args) + util.KubectlExec = func(args string) (out []byte, err error) { return []byte(`{"logger": {"level": 2}}`), nil } - c, err := cm.DeleteConfigMap(XAppConfig{}) + validationErrors, err := NewCM().DeleteConfigMap(models.ConfigMetadata{}) if err != nil { - t.Errorf("DeleteConfigMap failed: %v -> %v", err, c) + t.Errorf("DeleteConfigMap failed: %v -> %v", err, validationErrors) } } func TestPurgeConfigMapSuccess(t *testing.T) { - cm := ConfigMap{} - - HelmExec = func(args string) (out []byte, err error) { + util.HelmExec = func(args string) (out []byte, err error) { return []byte("ok"), nil } - KubectlExec = func(args string) (out []byte, err error) { + util.KubectlExec = func(args string) (out []byte, err error) { return []byte(`{"logger": {"level": 2}}`), nil } - c, err := cm.PurgeConfigMap(XappDeploy{}) + name := "dummy-xapp" + validationErrors, err := NewCM().PurgeConfigMap(models.XappDescriptor{XappName: &name}) if err != nil { - t.Errorf("PurgeConfigMap failed: %v -> %v", err, c) + t.Errorf("PurgeConfigMap failed: %v -> %v", err, validationErrors) } } func TestCreateConfigMapFails(t *testing.T) { - cm := ConfigMap{} - - c, err := cm.CreateConfigMap(XAppConfig{}) + name := "dummy-xapp" + validationErrors, err := NewCM().CreateConfigMap(models.XAppConfig{Metadata: &models.ConfigMetadata{Name: &name}}) if err == nil { - t.Errorf("CreateConfigMap failed: %v -> %v", err, c) + t.Errorf("CreateConfigMap failed: %v -> %v", err, validationErrors) } } func TestUpdateConfigMapFails(t *testing.T) { - cm := ConfigMap{} - - c, err := cm.UpdateConfigMap(XAppConfig{}) + name := "dummy-xapp" + validationErrors, err := NewCM().UpdateConfigMap(models.XAppConfig{Metadata: &models.ConfigMetadata{Name: &name}}) if err == nil { - t.Errorf("CreateConfigMap failed: %v -> %v", err, c) + t.Errorf("CreateConfigMap failed: %v -> %v", err, validationErrors) } } func TestValidationSuccess(t *testing.T) { - cm := ConfigMap{} var d interface{} var cfg map[string]interface{} - err := json.Unmarshal([]byte(`{"local": {"host": ":8080"}, "logger": {"level": 3}}`), &cfg) - err = cm.ReadFile("./test/schema.json", &d) + err = NewCM().ReadFile("../../test/schema.json", &d) if err != nil { t.Errorf("ReadFile failed: %v -> %v", err, d) } - feedback, err := cm.doValidate(d, cfg) + feedback, err := NewCM().doValidate(d, cfg) if err != nil { t.Errorf("doValidate failed: %v -> %v", err, feedback) } } func TestValidationFails(t *testing.T) { - cm := ConfigMap{} var d interface{} var cfg map[string]interface{} - err := json.Unmarshal([]byte(`{"local": {"host": ":8080"}, "logger": {"level": "INVALID"}}`), &cfg) - err = cm.ReadFile("./test/schema.json", &d) + err = NewCM().ReadFile("../../test/schema.json", &d) if err != nil { t.Errorf("ConfigMetadata failed: %v -> %v", err, d) } - feedback, err := cm.doValidate(d, cfg) + feedback, err := NewCM().doValidate(d, cfg) if err == nil { t.Errorf("doValidate should faile but didn't: %v -> %v", err, feedback) } - - log.Println("Feedbacks: ", feedback) + appmgr.Logger.Debug("Feedbacks: %v", feedback) } diff --git a/cmd/appmgr/helm.go b/pkg/helm/helm.go similarity index 57% rename from cmd/appmgr/helm.go rename to pkg/helm/helm.go index 8a8154a..a3d7b18 100755 --- a/cmd/appmgr/helm.go +++ b/pkg/helm/helm.go @@ -17,62 +17,32 @@ ================================================================================== */ -package main +package helm import ( - "bytes" - "errors" "fmt" + "github.com/ghodss/yaml" "github.com/spf13/viper" "io/ioutil" - "log" "os" - "os/exec" "regexp" "strconv" "strings" "time" -) - -var execCommand = exec.Command - -func Exec(args string) (out []byte, err error) { - cmd := execCommand("/bin/sh", "-c", args) - - var stdout bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - log.Println("Running command: ", cmd) - for i := 0; i < viper.GetInt("helm.retry"); i++ { - err = cmd.Run() - if err != nil { - Logger.Error("Command '%s' failed with error: %v, retrying", args, err.Error()+stderr.String()) - time.Sleep(time.Duration(2) * time.Second) - continue - } - break - } - if err == nil && !strings.HasSuffix(os.Args[0], ".test") { - Logger.Info("command success: %s", stdout.String()) - return stdout.Bytes(), nil - } - - return stdout.Bytes(), errors.New(stderr.String()) -} - -var HelmExec = func(args string) (out []byte, err error) { - return Exec(strings.Join([]string{"helm", args}, " ")) -} + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/cm" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/util" +) -var KubectlExec = func(args string) (out []byte, err error) { - return Exec(strings.Join([]string{"kubectl", args}, " ")) +type Helm struct { + initDone bool + cm *cm.CM } -func (h *Helm) SetCM(cm ConfigMapper) { - h.cm = cm +func NewHelm() *Helm { + return &Helm{initDone: false, cm: cm.NewCM()} } func (h *Helm) Initialize() { @@ -82,115 +52,108 @@ func (h *Helm) Initialize() { for { if _, err := h.Init(); err == nil { - Logger.Info("Helm init done successfully!") + appmgr.Logger.Info("Helm init done successfully!") break } - Logger.Error("helm init failed, retyring ...") + appmgr.Logger.Info("helm init failed, retyring ...") time.Sleep(time.Duration(10) * time.Second) } for { if _, err := h.AddRepo(); err == nil { - Logger.Info("Helm repo added successfully") + appmgr.Logger.Info("Helm repo added successfully") break } - Logger.Error("Helm repo addition failed, retyring ...") + appmgr.Logger.Info("Helm repo addition failed, retyring ...") time.Sleep(time.Duration(10) * time.Second) } - h.initDone = true } func (h *Helm) Run(args string) (out []byte, err error) { - return HelmExec(args) + return util.HelmExec(args) } // API functions func (h *Helm) Init() (out []byte, err error) { - // Add Tiller address as environment variable - if err := addTillerEnv(); err != nil { + if err := h.AddTillerEnv(); err != nil { return out, err } - return HelmExec(strings.Join([]string{"init -c --skip-refresh"}, "")) + return util.HelmExec(strings.Join([]string{"init -c --skip-refresh"}, "")) } 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 { - Logger.Error("helm_repo_username ReadFile failed: %v", err.Error()) + appmgr.Logger.Info("helm_repo_username ReadFile failed: %v", err.Error()) return } - username := " --username " + string(credFile) credFile, err = ioutil.ReadFile(viper.GetString("helm.helm-password-file")) if err != nil { - Logger.Error("helm_repo_password ReadFile failed: %v", err.Error()) + appmgr.Logger.Info("helm_repo_password ReadFile failed: %v", 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 HelmExec(strings.Join([]string{"repo add ", rname, " ", repo, username, pwd}, "")) + return util.HelmExec(strings.Join([]string{"repo add ", rname, " ", repo, username, pwd}, "")) } -func (h *Helm) Install(m XappDeploy) (xapp Xapp, err error) { +func (h *Helm) Install(m models.XappDescriptor) (xapp models.Xapp, err error) { + var c interface{} + m.Namespace = h.cm.GetNamespace(m.Namespace) + out, err := h.Run(strings.Join([]string{"repo update "}, "")) if err != nil { return } - var cm interface{} - m.Namespace = h.cm.GetNamespace(m.Namespace) - - if err = h.cm.GetConfigMap(m, &cm); err != nil { - out, err = h.Run(getInstallArgs(m, false)) + if err = h.cm.GetConfigMap(m, &c); err != nil { + out, err = h.Run(h.GetInstallArgs(m, false)) if err != nil { return } - return h.ParseStatus(m.Name, string(out)) + return h.ParseStatus(*m.XappName, string(out)) } // ConfigMap exists, try to override - out, err = h.Run(getInstallArgs(m, true)) + out, err = h.Run(h.GetInstallArgs(m, true)) if err == nil { - return h.ParseStatus(m.Name, string(out)) + return h.ParseStatus(*m.XappName, string(out)) } - cm, cmErr := h.cm.PurgeConfigMap(m) - out, err = h.Run(getInstallArgs(m, false)) + c, cmErr := h.cm.PurgeConfigMap(m) + out, err = h.Run(h.GetInstallArgs(m, false)) if err != nil { return } if cmErr == nil { - cmErr = h.cm.RestoreConfigMap(m, cm) + cmErr = h.cm.RestoreConfigMap(m, c) } - return h.ParseStatus(m.Name, string(out)) + return h.ParseStatus(*m.XappName, string(out)) } -func (h *Helm) Status(name string) (xapp Xapp, err error) { +func (h *Helm) Status(name string) (xapp models.Xapp, err error) { out, err := h.Run(strings.Join([]string{"status ", name}, "")) if err != nil { - Logger.Error("Getting xapps status: %v", err.Error()) + appmgr.Logger.Info("Getting xapps status: %v", err.Error()) return } return h.ParseStatus(name, string(out)) } -func (h *Helm) StatusAll() (xapps []Xapp, err error) { +func (h *Helm) StatusAll() (xapps models.AllDeployedXapps, err error) { xappNameList, err := h.List() if err != nil { - Logger.Error("Helm list failed: %v", err.Error()) + appmgr.Logger.Info("Helm list failed: %v", err.Error()) return } @@ -199,23 +162,23 @@ func (h *Helm) StatusAll() (xapps []Xapp, err error) { func (h *Helm) List() (names []string, err error) { ns := h.cm.GetNamespace("") - out, err := h.Run(strings.Join([]string{"list --all --output yaml --namespace=", ns}, "")) + out, err := h.Run(strings.Join([]string{"list --all --deployed --output yaml --namespace=", ns}, "")) if err != nil { - Logger.Error("Listing deployed xapps failed: %v", err.Error()) + appmgr.Logger.Info("Listing deployed xapps failed: %v", err.Error()) return } return h.GetNames(string(out)) } -func (h *Helm) SearchAll() (names []string) { +func (h *Helm) SearchAll() models.AllDeployableXapps { return h.cm.GetNamesFromHelmRepo() } -func (h *Helm) Delete(name string) (xapp Xapp, err error) { +func (h *Helm) Delete(name string) (xapp models.Xapp, err error) { xapp, err = h.Status(name) if err != nil { - Logger.Error("Fetching xapp status failed: %v", err.Error()) + appmgr.Logger.Info("Fetching xapp status failed: %v", err.Error()) return } @@ -237,7 +200,7 @@ func (h *Helm) Fetch(name, tarDir string) error { // Helper functions func (h *Helm) GetVersion(name string) (version string) { ns := h.cm.GetNamespace("") - out, err := h.Run(strings.Join([]string{"list --output yaml --namespace=", ns, " ", name}, "")) + out, err := h.Run(strings.Join([]string{"list --deployed --output yaml --namespace=", ns, " ", name}, "")) if err != nil { return } @@ -276,14 +239,12 @@ func (h *Helm) GetAddress(out string) (ip, port string) { func (h *Helm) GetEndpointInfo(name string) (ip string, port int) { ns := h.cm.GetNamespace("") args := fmt.Sprintf(" get endpoints -o=jsonpath='{.subsets[*].addresses[*].ip}' service-%s-%s-rmr -n %s", ns, name, ns) - out, err := KubectlExec(args) + out, err := util.KubectlExec(args) if err != nil { return } - Logger.Info("Endpoint IP address of %s: %s", name, string(out)) - - // "service---rmr." - return "service-" + ns + "-" + name + "-rmr." + ns, 4560 + appmgr.Logger.Info("Endpoint IP address of %s: %s", name, string(out)) + return fmt.Sprintf("service-%s-%s-rmr.%s", ns, name, ns), 4560 } func (h *Helm) GetNames(out string) (names []string, err error) { @@ -302,10 +263,10 @@ func (h *Helm) GetNames(out string) (names []string, err error) { return names, nil } -func (h *Helm) FillInstanceData(name string, out string, xapp *Xapp, msgs MessageTypes) { +func (h *Helm) FillInstanceData(name string, out string, xapp *models.Xapp, msgs appmgr.MessageTypes) { ip, port := h.GetEndpointInfo(name) if ip == "" { - Logger.Info("Endpoint IP address not found, using CluserIP") + appmgr.Logger.Info("Endpoint IP address not found, using CluserIP") ip, _ = h.GetAddress(out) } @@ -320,77 +281,88 @@ func (h *Helm) FillInstanceData(name string, out string, xapp *Xapp, msgs Messag 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) + var x models.XappInstance + var name string + fmt.Sscanf(v[0], "%s %s %s", &name, &tmp, &x.Status) + x.Name = &name x.Status = strings.ToLower(x.Status) - x.Ip = ip - x.Port = port + x.IP = ip + x.Port = int64(port) x.TxMessages = msgs.TxMessages x.RxMessages = msgs.RxMessages - xapp.Instances = append(xapp.Instances, x) + xapp.Instances = append(xapp.Instances, &x) } } } -func (h *Helm) ParseStatus(name string, out string) (xapp Xapp, err error) { - xapp.Name = name +func (h *Helm) ParseStatus(name string, out string) (xapp models.Xapp, err error) { + xapp.Name = &name xapp.Version = h.GetVersion(name) xapp.Status = h.GetState(out) h.FillInstanceData(name, out, &xapp, h.cm.GetMessages(name)) - return } -func (h *Helm) parseAllStatus(names []string) (xapps []Xapp, err error) { - xapps = []Xapp{} - +func (h *Helm) parseAllStatus(names []string) (xapps models.AllDeployedXapps, err error) { + xapps = models.AllDeployedXapps{} for _, name := range names { - err := h.cm.ReadSchema(name, &XAppConfig{}) + err := h.cm.ReadSchema(name, &models.XAppConfig{}) if err != nil { continue } x, err := h.Status(name) if err == nil { - xapps = append(xapps, x) + xapps = append(xapps, &x) } } - return } -func addTillerEnv() (err error) { +func (h *Helm) 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 { - Logger.Error("Tiller Env Setting Failed: %v", err.Error()) + appmgr.Logger.Info("Tiller Env Setting Failed: %v", err.Error()) } - return err } -func getInstallArgs(x XappDeploy, cmOverride bool) (args string) { +func (h *Helm) GetInstallArgs(x models.XappDescriptor, cmOverride bool) (args string) { args = args + " --namespace=" + x.Namespace - - if x.ImageRepo != "" { - args = args + " --set global.repository=" + x.ImageRepo - } - - if x.ServiceName != "" { - args = args + " --set ricapp.service.name=" + x.ServiceName + if x.HelmVersion != "" { + args = args + " --version=" + x.HelmVersion } - if x.Hostname != "" { - args = args + " --set ricapp.hostname=" + x.Hostname + if x.ReleaseName != "" { + args = args + " --name=" + x.ReleaseName + } else { + args = args + " --name=" + *x.XappName } if cmOverride == true { - args = args + " --set ricapp.appconfig.override=" + x.Name + "-appconfig" + args = args + " --set ricapp.appconfig.override=" + *x.XappName + "-appconfig" + } + + if x.OverrideFile != nil { + if overrideYaml, err := yaml.JSONToYAML([]byte(x.OverrideFile.(string))); err == nil { + err = ioutil.WriteFile("/tmp/appmgr_override.yaml", overrideYaml, 0644) + if err != nil { + appmgr.Logger.Info("ioutil.WriteFile(/tmp/appmgr_override.yaml) failed: %v", err) + } else { + args = args + " -f=/tmp/appmgr_override.yaml" + } + } else { + appmgr.Logger.Info("yaml.JSONToYAML failed: %v", err) + } } - rname := viper.GetString("helm.repo-name") - return fmt.Sprintf("install %s/%s --name=%s %s", rname, x.Name, x.Name, args) + repoName := viper.GetString("helm.repo-name") + if repoName == "" { + repoName = "helm-repo" + } + return fmt.Sprintf("install %s/%s %s", repoName, *x.XappName, args) } diff --git a/cmd/appmgr/helm_test.go b/pkg/helm/helm_test.go similarity index 53% rename from cmd/appmgr/helm_test.go rename to pkg/helm/helm_test.go index 6ad10c6..fb72358 100755 --- a/cmd/appmgr/helm_test.go +++ b/pkg/helm/helm_test.go @@ -17,11 +17,17 @@ ================================================================================== */ -package main +package helm import ( + "os" "reflect" + "strconv" "testing" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/util" ) var helmStatusOutput = ` @@ -69,28 +75,42 @@ Releases: Status: DEPLOYED Updated: Sun Mar 24 07:17:00 2019` -var h = Helm{} +// Test cases +func TestMain(m *testing.M) { + appmgr.Init() + appmgr.Logger.SetLevel(0) + + code := m.Run() + os.Exit(code) +} func TestHelmStatus(t *testing.T) { - h.SetCM(&ConfigMap{}) - KubectlExec = func(args string) (out []byte, err error) { + //NewHelm().SetCM(&ConfigMap{}) + util.KubectlExec = func(args string) (out []byte, err error) { return []byte("10.102.184.212"), nil } - xapp, err := h.ParseStatus("dummy-xapp", helmStatusOutput) + xapp, err := NewHelm().ParseStatus("dummy-xapp", helmStatusOutput) if err != nil { t.Errorf("Helm install failed: %v", err) } - x := getXappData() xapp.Version = "1.0" - if !reflect.DeepEqual(xapp, x) { - t.Errorf("\n%v \n%v", xapp, x) + if *x.Name != *xapp.Name || x.Status != xapp.Status || x.Version != xapp.Version { + t.Errorf("\n%v \n%v", *xapp.Name, *x.Name) + } + + if *x.Instances[0].Name != *xapp.Instances[0].Name || x.Instances[0].Status != xapp.Instances[0].Status { + t.Errorf("\n1:%v 2:%v", *x.Instances[0].Name, *xapp.Instances[0].Name) + } + + if x.Instances[0].IP != xapp.Instances[0].IP || x.Instances[0].Port != xapp.Instances[0].Port { + t.Errorf("\n1:%v 2:%v", x.Instances[0].IP, xapp.Instances[0].IP) } } func TestHelmLists(t *testing.T) { - names, err := h.GetNames(helListOutput) + names, err := NewHelm().GetNames(helListOutput) if err != nil { t.Errorf("Helm status failed: %v", err) } @@ -101,44 +121,61 @@ func TestHelmLists(t *testing.T) { } func TestAddTillerEnv(t *testing.T) { - if addTillerEnv() != nil { + if NewHelm().AddTillerEnv() != nil { t.Errorf("TestAddTillerEnv failed!") } } func TestGetInstallArgs(t *testing.T) { - x := XappDeploy{Name: "dummy-xapp", Namespace: "ricxapp"} + name := "dummy-xapp" + x := models.XappDescriptor{XappName: &name, Namespace: "ricxapp"} - expectedArgs := "install helm-repo/dummy-xapp --name=dummy-xapp --namespace=ricxapp" - if args := getInstallArgs(x, false); args != expectedArgs { + expectedArgs := "install helm-repo/dummy-xapp --namespace=ricxapp --name=dummy-xapp" + if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) } - x.ImageRepo = "localhost:5000" - expectedArgs = expectedArgs + " --set global.repository=" + "localhost:5000" - if args := getInstallArgs(x, false); args != expectedArgs { + x.HelmVersion = "1.2.3" + expectedArgs = "install helm-repo/dummy-xapp --namespace=ricxapp --version=1.2.3 --name=dummy-xapp" + if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) } - x.ServiceName = "xapp" - expectedArgs = expectedArgs + " --set ricapp.service.name=" + "xapp" - if args := getInstallArgs(x, false); args != expectedArgs { - t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) - } - - x.ServiceName = "xapp" - expectedArgs = expectedArgs + " --set ricapp.appconfig.override=dummy-xapp-appconfig" - if args := getInstallArgs(x, true); args != expectedArgs { + x.ReleaseName = "ueec-xapp" + expectedArgs = "install helm-repo/dummy-xapp --namespace=ricxapp --version=1.2.3 --name=ueec-xapp" + if args := NewHelm().GetInstallArgs(x, false); args != expectedArgs { t.Errorf("TestGetInstallArgs failed: expected %v, got %v", expectedArgs, args) } } -func getXappData() (x Xapp) { - x = generateXapp("dummy-xapp", "deployed", "1.0", "dummy-xapp-8984fc9fd-bkcbp", "running", "10.102.184.212", "4560") - 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" +func getXappData() (x models.Xapp) { + //name1 := "dummy-xapp-8984fc9fd-l6xch" + //name2 := "dummy-xapp-8984fc9fd-pp4hg" + x = generateXapp("dummy-xapp", "deployed", "1.0", "dummy-xapp-8984fc9fd-bkcbp", "running", "service-ricxapp-dummy-xapp-rmr.ricxapp", "4560") + //x.Instances = append(x.Instances, x.Instances[0]) + //x.Instances = append(x.Instances, x.Instances[0]) + //x.Instances[1].Name = &name1 + //x.Instances[2].Name = &name2 return x } + +func generateXapp(name, status, ver, iname, istatus, ip, port string) (x models.Xapp) { + x.Name = &name + x.Status = status + x.Version = ver + p, _ := strconv.Atoi(port) + var msgs appmgr.MessageTypes + + instance := &models.XappInstance{ + Name: &iname, + Status: istatus, + IP: ip, + Port: int64(p), + TxMessages: msgs.TxMessages, + RxMessages: msgs.RxMessages, + } + x.Instances = append(x.Instances, instance) + + return +} diff --git a/cmd/appmgr/logger.go b/pkg/logger/logger.go similarity index 87% rename from cmd/appmgr/logger.go rename to pkg/logger/logger.go index d1bcf15..251e90f 100755 --- a/cmd/appmgr/logger.go +++ b/pkg/logger/logger.go @@ -17,11 +17,10 @@ ================================================================================== */ -package main +package logger import ( mdclog "gerrit.o-ran-sc.org/r/com/golog" - "net/http" "time" ) @@ -63,10 +62,3 @@ func (l *Log) Debug(pattern string, args ...interface{}) { l.SetMdc("time", time.Now().Format(time.RFC3339)) l.logger.Debug(pattern, args...) } - -func LogRestRequests(inner http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - inner.ServeHTTP(w, r) - Logger.Info("Logger: method=%s url=%s", r.Method, r.URL.RequestURI()) - }) -} diff --git a/pkg/restful/restful.go b/pkg/restful/restful.go new file mode 100755 index 0000000..c0ef580 --- /dev/null +++ b/pkg/restful/restful.go @@ -0,0 +1,245 @@ +/* + ================================================================================== + 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 restful + +import ( + //"github.com/spf13/viper" + "log" + "os" + "time" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/restapi" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/restapi/operations" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/restapi/operations/health" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/restapi/operations/xapp" + "github.com/go-openapi/loads" + "github.com/go-openapi/runtime/middleware" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/cm" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/helm" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/resthooks" +) + +func NewRestful() *Restful { + r := &Restful{ + helm: helm.NewHelm(), + cm: cm.NewCM(), + rh: resthooks.NewResthook(), + ready: false, + } + r.api = r.SetupHandler() + return r +} + +func (r *Restful) Run() { + server := restapi.NewServer(r.api) + defer server.Shutdown() + server.Port = 8080 //viper.GetInt("local.port") + server.Host = "0.0.0.0" //viper.GetString("local.host") + + appmgr.Logger.Info("Xapp manager started ... serving on %s:%d\n", server.Host, server.Port) + + go r.NotifyClients() + if err := server.Serve(); err != nil { + log.Fatal(err.Error()) + } +} + +func (r *Restful) SetupHandler() *operations.AppManagerAPI { + swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON) + if err != nil { + appmgr.Logger.Error(err.Error()) + os.Exit(1) + } + api := operations.NewAppManagerAPI(swaggerSpec) + + // URL: /ric/v1/health + api.HealthGetHealthAliveHandler = health.GetHealthAliveHandlerFunc( + func(params health.GetHealthAliveParams) middleware.Responder { + return health.NewGetHealthAliveOK() + }) + api.HealthGetHealthReadyHandler = health.GetHealthReadyHandlerFunc( + func(params health.GetHealthReadyParams) middleware.Responder { + return health.NewGetHealthReadyOK() + }) + + // URL: /ric/v1/subscriptions + api.GetSubscriptionsHandler = operations.GetSubscriptionsHandlerFunc( + func(params operations.GetSubscriptionsParams) middleware.Responder { + return operations.NewGetSubscriptionsOK().WithPayload(r.rh.GetAllSubscriptions()) + }) + api.GetSubscriptionByIDHandler = operations.GetSubscriptionByIDHandlerFunc( + func(params operations.GetSubscriptionByIDParams) middleware.Responder { + if result, found := r.rh.GetSubscriptionById(params.SubscriptionID); found { + return operations.NewGetSubscriptionByIDOK().WithPayload(&result) + } + return operations.NewGetSubscriptionByIDNotFound() + }) + api.AddSubscriptionHandler = operations.AddSubscriptionHandlerFunc( + func(params operations.AddSubscriptionParams) middleware.Responder { + return operations.NewAddSubscriptionCreated().WithPayload(r.rh.AddSubscription(*params.SubscriptionRequest)) + }) + api.ModifySubscriptionHandler = operations.ModifySubscriptionHandlerFunc( + func(params operations.ModifySubscriptionParams) middleware.Responder { + if _, ok := r.rh.ModifySubscription(params.SubscriptionID, *params.SubscriptionRequest); ok { + return operations.NewModifySubscriptionOK() + } + return operations.NewModifySubscriptionBadRequest() + }) + api.DeleteSubscriptionHandler = operations.DeleteSubscriptionHandlerFunc( + func(params operations.DeleteSubscriptionParams) middleware.Responder { + if _, ok := r.rh.DeleteSubscription(params.SubscriptionID); ok { + return operations.NewDeleteSubscriptionNoContent() + } + return operations.NewDeleteSubscriptionBadRequest() + }) + + // URL: /ric/v1/xapp + api.XappGetAllXappsHandler = xapp.GetAllXappsHandlerFunc( + func(params xapp.GetAllXappsParams) middleware.Responder { + if result, err := r.helm.StatusAll(); err == nil { + return xapp.NewGetAllXappsOK().WithPayload(result) + } + return xapp.NewGetAllXappsInternalServerError() + }) + api.XappListAllXappsHandler = xapp.ListAllXappsHandlerFunc( + func(params xapp.ListAllXappsParams) middleware.Responder { + if result := r.helm.SearchAll(); err == nil { + return xapp.NewListAllXappsOK().WithPayload(result) + } + return xapp.NewListAllXappsInternalServerError() + }) + api.XappGetXappByNameHandler = xapp.GetXappByNameHandlerFunc( + func(params xapp.GetXappByNameParams) middleware.Responder { + if result, err := r.helm.Status(params.XAppName); err == nil { + return xapp.NewGetXappByNameOK().WithPayload(&result) + } + return xapp.NewGetXappByNameNotFound() + }) + api.XappGetXappInstanceByNameHandler = xapp.GetXappInstanceByNameHandlerFunc( + func(params xapp.GetXappInstanceByNameParams) middleware.Responder { + if result, err := r.helm.Status(params.XAppName); err == nil { + for _, v := range result.Instances { + if *v.Name == params.XAppInstanceName { + return xapp.NewGetXappInstanceByNameOK().WithPayload(v) + } + } + } + return xapp.NewGetXappInstanceByNameNotFound() + }) + api.XappDeployXappHandler = xapp.DeployXappHandlerFunc( + func(params xapp.DeployXappParams) middleware.Responder { + if result, err := r.helm.Install(*params.XappDescriptor); err == nil { + go r.PublishXappCreateEvent(params) + return xapp.NewDeployXappCreated().WithPayload(&result) + } + return xapp.NewUndeployXappInternalServerError() + }) + api.XappUndeployXappHandler = xapp.UndeployXappHandlerFunc( + func(params xapp.UndeployXappParams) middleware.Responder { + if result, err := r.helm.Delete(params.XAppName); err == nil { + go r.PublishXappDeleteEvent(result) + return xapp.NewUndeployXappNoContent() + } + return xapp.NewUndeployXappInternalServerError() + }) + + // URL: /ric/v1/config + api.XappGetAllXappConfigHandler = xapp.GetAllXappConfigHandlerFunc( + func(params xapp.GetAllXappConfigParams) middleware.Responder { + return xapp.NewGetAllXappConfigOK().WithPayload(r.cm.UploadConfig()) + }) + api.XappCreateXappConfigHandler = xapp.CreateXappConfigHandlerFunc( + func(params xapp.CreateXappConfigParams) middleware.Responder { + result, err := r.cm.CreateConfigMap(*params.XAppConfig) + if err == nil { + if err.Error() != "Validation failed!" { + return xapp.NewCreateXappConfigInternalServerError() + } else { + return xapp.NewCreateXappConfigUnprocessableEntity() + } + } + r.rh.PublishSubscription(models.Xapp{}, models.EventTypeCreated) + return xapp.NewCreateXappConfigCreated().WithPayload(result) + }) + api.XappModifyXappConfigHandler = xapp.ModifyXappConfigHandlerFunc( + func(params xapp.ModifyXappConfigParams) middleware.Responder { + result, err := r.cm.UpdateConfigMap(*params.XAppConfig) + if err == nil { + if err.Error() != "Validation failed!" { + return xapp.NewModifyXappConfigInternalServerError() + } else { + return xapp.NewModifyXappConfigUnprocessableEntity() + } + } + r.rh.PublishSubscription(models.Xapp{}, models.EventTypeModified) + return xapp.NewModifyXappConfigOK().WithPayload(result) + }) + api.XappDeleteXappConfigHandler = xapp.DeleteXappConfigHandlerFunc( + func(params xapp.DeleteXappConfigParams) middleware.Responder { + _, err := r.cm.DeleteConfigMap(*params.ConfigMetadata) + if err == nil { + return xapp.NewDeleteXappConfigInternalServerError() + } + r.rh.PublishSubscription(models.Xapp{}, models.EventTypeDeleted) + return xapp.NewDeleteXappConfigNoContent() + }) + + // LCM: /xapps/{xAppName}/instances/{xAppInstanceName}/stop/start + api.XappStartXappInstanceByNameHandler = xapp.StartXappInstanceByNameHandlerFunc( + func(params xapp.StartXappInstanceByNameParams) middleware.Responder { + return xapp.NewStartXappInstanceByNameOK() + }) + api.XappStopXappInstanceByNameHandler = xapp.StopXappInstanceByNameHandlerFunc( + func(params xapp.StopXappInstanceByNameParams) middleware.Responder { + return xapp.NewStopXappInstanceByNameOK() + }) + + return api +} + +func (r *Restful) NotifyClients() { + r.helm.Initialize() + if xapps, err := r.helm.StatusAll(); err == nil { + r.rh.NotifyClients(xapps, models.EventTypeRestarted) + r.ready = true + } +} + +func (r *Restful) PublishXappCreateEvent(params xapp.DeployXappParams) { + name := *params.XappDescriptor.XappName + if params.XappDescriptor.ReleaseName != "" { + name = params.XappDescriptor.ReleaseName + } + + for i := 0; i < 5; i++ { + if result, _ := r.helm.Status(name); result.Instances != nil { + r.rh.PublishSubscription(result, models.EventTypeDeployed) + break + } + time.Sleep(time.Duration(5) * time.Second) + } +} + +func (r *Restful) PublishXappDeleteEvent(xapp models.Xapp) { + r.rh.PublishSubscription(xapp, models.EventTypeUndeployed) +} diff --git a/pkg/restful/types.go b/pkg/restful/types.go new file mode 100755 index 0000000..189962d --- /dev/null +++ b/pkg/restful/types.go @@ -0,0 +1,49 @@ +/* +================================================================================== + 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 restful + +import ( + "net/http" + + cfgmap "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/cm" + helmer "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/helm" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/restapi/operations" + resthook "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/resthooks" +) + +type CmdOptions struct { + hostAddr *string + helmHost *string + helmChartPath *string +} + +type Resource struct { + Method string + Url string + HandlerFunc http.HandlerFunc +} + +type Restful struct { + api *operations.AppManagerAPI + helm *helmer.Helm + cm *cfgmap.CM + rh *resthook.Resthook + ready bool +} diff --git a/pkg/resthooks/resthooks.go b/pkg/resthooks/resthooks.go new file mode 100755 index 0000000..5076b06 --- /dev/null +++ b/pkg/resthooks/resthooks.go @@ -0,0 +1,224 @@ +/* +================================================================================== + 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 resthooks + +import ( + "bytes" + "encoding/json" + sdl "gerrit.oran-osc.org/r/ric-plt/sdlgo" + cmap "github.com/orcaman/concurrent-map" + "github.com/segmentio/ksuid" + "net/http" + "time" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" +) + +func NewResthook() *Resthook { + rh := &Resthook{ + client: &http.Client{}, + db: sdl.NewSdlInstance("appmgr", sdl.NewDatabase()), + } + + rh.subscriptions = rh.RestoreSubscriptions() + return rh +} + +func (rh *Resthook) AddSubscription(sr models.SubscriptionRequest) *models.SubscriptionResponse { + for v := range rh.subscriptions.IterBuffered() { + r := v.Val.(SubscriptionInfo).req + if *r.Data.TargetURL == *sr.Data.TargetURL && r.Data.EventType == sr.Data.EventType { + appmgr.Logger.Info("Similar subscription already exists!") + return &models.SubscriptionResponse{} + } + } + + key := ksuid.New().String() + resp := models.SubscriptionResponse{ID: key, Version: 0, EventType: sr.Data.EventType} + rh.subscriptions.Set(key, SubscriptionInfo{key, sr, resp}) + rh.StoreSubscriptions(rh.subscriptions) + + appmgr.Logger.Info("Sub: New subscription added: key=%s targetUl=%s eventType=%s", key, *sr.Data.TargetURL, sr.Data.EventType) + return &resp +} + +func (rh *Resthook) DeleteSubscription(id string) (*models.SubscriptionResponse, bool) { + if v, found := rh.subscriptions.Get(id); found { + appmgr.Logger.Info("Subscription id=%s found: %v ... deleting", id, v.(SubscriptionInfo).req) + + rh.subscriptions.Remove(id) + rh.StoreSubscriptions(rh.subscriptions) + resp := v.(SubscriptionInfo).resp + return &resp, found + } + return &models.SubscriptionResponse{}, false +} + +func (rh *Resthook) ModifySubscription(id string, req models.SubscriptionRequest) (*models.SubscriptionResponse, bool) { + if s, found := rh.subscriptions.Get(id); found { + appmgr.Logger.Info("Subscription id=%s found: %v ... updating", id, s.(SubscriptionInfo).req) + + resp := models.SubscriptionResponse{ID: id, Version: 0, EventType: req.Data.EventType} + rh.subscriptions.Set(id, SubscriptionInfo{id, req, resp}) + rh.StoreSubscriptions(rh.subscriptions) + + return &resp, found + } + return &models.SubscriptionResponse{}, false +} + +func (rh *Resthook) GetAllSubscriptions() (hooks models.AllSubscriptions) { + hooks = models.AllSubscriptions{} + for v := range rh.subscriptions.IterBuffered() { + s := v.Val.(SubscriptionInfo) + r := v.Val.(SubscriptionInfo).req + hooks = append(hooks, &models.Subscription{&models.SubscriptionData{r.Data.EventType, r.Data.MaxRetries, r.Data.RetryTimer, r.Data.TargetURL}, s.Id}) + } + + return hooks +} + +func (rh *Resthook) GetSubscriptionById(id string) (models.Subscription, bool) { + if v, found := rh.subscriptions.Get(id); found { + appmgr.Logger.Info("Subscription id=%s found: %v", id, v.(SubscriptionInfo).req) + r := v.(SubscriptionInfo).req + return models.Subscription{&models.SubscriptionData{r.Data.EventType, r.Data.MaxRetries, r.Data.RetryTimer, r.Data.TargetURL}, id}, found + } + return models.Subscription{}, false +} + +func (rh *Resthook) PublishSubscription(x models.Xapp, et models.EventType) { + rh.NotifyClients(models.AllDeployedXapps{&x}, et) +} + +func (rh *Resthook) NotifyClients(xapps models.AllDeployedXapps, et models.EventType) { + if len(xapps) == 0 || len(rh.subscriptions) == 0 { + appmgr.Logger.Info("Nothing to publish [%d:%d]", len(xapps), len(rh.subscriptions)) + return + } + + rh.Seq = rh.Seq + 1 + for v := range rh.subscriptions.Iter() { + go rh.notify(xapps, et, v.Val.(SubscriptionInfo), rh.Seq) + } +} + +func (rh *Resthook) notify(xapps models.AllDeployedXapps, et models.EventType, s SubscriptionInfo, seq int64) error { + notif := models.SubscriptionNotification{ID: s.Id, Version: seq, EventType: et, XApps: xapps} + jsonData, err := json.Marshal(notif) + if err != nil { + appmgr.Logger.Info("json.Marshal failed: %v", err) + return err + } + + // Execute the request with retry policy + return rh.retry(s, func() error { + appmgr.Logger.Info("Posting notification to TargetURL=%s: %v", *s.req.Data.TargetURL, notif) + resp, err := http.Post(*s.req.Data.TargetURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + appmgr.Logger.Info("Posting to subscription failed: %v", err) + return err + } + + if resp.StatusCode != http.StatusOK { + appmgr.Logger.Info("Client returned error code: %d", resp.StatusCode) + return err + } + + appmgr.Logger.Info("subscription to '%s' dispatched, response code: %d", *s.req.Data.TargetURL, resp.StatusCode) + return nil + }) +} + +func (rh *Resthook) retry(s SubscriptionInfo, fn func() error) error { + if err := fn(); err != nil { + // Todo: use exponential backoff, or similar mechanism + if *s.req.Data.MaxRetries--; *s.req.Data.MaxRetries > 0 { + time.Sleep(time.Duration(*s.req.Data.RetryTimer) * time.Second) + return rh.retry(s, fn) + } + rh.subscriptions.Remove(s.Id) + return err + } + return nil +} + +func (rh *Resthook) StoreSubscriptions(m cmap.ConcurrentMap) { + for v := range m.Iter() { + s := v.Val.(SubscriptionInfo) + + data, err := json.Marshal(s.req) + if err != nil { + appmgr.Logger.Error("json.marshal failed: %v ", err.Error()) + return + } + + if err := rh.db.Set(s.Id, data); err != nil { + appmgr.Logger.Error("DB.session.Set failed: %v ", err.Error()) + } + } +} + +func (rh *Resthook) RestoreSubscriptions() (m cmap.ConcurrentMap) { + rh.VerifyDBConnection() + + m = cmap.New() + keys, err := rh.db.GetAll() + if err != nil { + appmgr.Logger.Error("DB.session.GetAll failed: %v ", err.Error()) + return + } + + for _, key := range keys { + value, err := rh.db.Get([]string{key}) + if err != nil { + appmgr.Logger.Error("DB.session.Get failed: %v ", err.Error()) + return + } + + var item models.SubscriptionRequest + if err = json.Unmarshal([]byte(value[key].(string)), &item); err != nil { + appmgr.Logger.Error("json.Unmarshal failed: %v ", err.Error()) + return + } + + resp := models.SubscriptionResponse{ID: key, Version: 0, EventType: item.Data.EventType} + m.Set(key, SubscriptionInfo{key, item, resp}) + } + + return m +} + +func (rh *Resthook) VerifyDBConnection() { + // Test DB connection, and wait until ready! + for { + if _, err := rh.db.GetAll(); err == nil { + return + } + appmgr.Logger.Error("Database connection not ready, waiting ...") + time.Sleep(time.Duration(5 * time.Second)) + } +} + +func (rh *Resthook) FlushSubscriptions() { + rh.db.RemoveAll() + rh.subscriptions = cmap.New() +} diff --git a/pkg/resthooks/resthooks_test.go b/pkg/resthooks/resthooks_test.go new file mode 100755 index 0000000..018dee8 --- /dev/null +++ b/pkg/resthooks/resthooks_test.go @@ -0,0 +1,124 @@ +/* +================================================================================== + 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 resthooks + +import ( + "github.com/stretchr/testify/assert" + "os" + "testing" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" +) + +var resp models.SubscriptionResponse + +// Test cases +func TestMain(m *testing.M) { + appmgr.Init() + appmgr.Logger.SetLevel(0) + + code := m.Run() + os.Exit(code) +} + +func TestAddSubscriptionSuccess(t *testing.T) { + resp := NewResthook().AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventTypeCreated) +} + +func TestAddSubscriptionExists(t *testing.T) { + resp := NewResthook().AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventType("")) +} + +func TestDeletesubscriptionSuccess(t *testing.T) { + resp := NewResthook().AddSubscription(CreateSubscription(models.EventTypeDeleted, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventTypeDeleted) + + resp, ok := NewResthook().DeleteSubscription(resp.ID) + assert.Equal(t, ok, true) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventTypeDeleted) +} + +func TestDeletesubscriptionInvalid(t *testing.T) { + resp, ok := NewResthook().DeleteSubscription("Non-existent-ID") + assert.Equal(t, ok, false) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventType("")) +} + +func TestModifySubscriptionSuccess(t *testing.T) { + resp := NewResthook().AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventTypeCreated) + + resp, ok := NewResthook().ModifySubscription(resp.ID, CreateSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + assert.Equal(t, ok, true) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventTypeModified) +} + +func TestModifysubscriptionInvalid(t *testing.T) { + resp, ok := NewResthook().DeleteSubscription("Non-existent-ID") + assert.Equal(t, ok, false) + assert.Equal(t, resp.Version, int64(0)) + assert.Equal(t, resp.EventType, models.EventType("")) +} + +func TestGetAllSubscriptionSuccess(t *testing.T) { + NewResthook().FlushSubscriptions() + subscriptions := NewResthook().GetAllSubscriptions() + assert.Equal(t, len(subscriptions), 0) + + NewResthook().AddSubscription(CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook")) + NewResthook().AddSubscription(CreateSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2")) + + subscriptions = NewResthook().GetAllSubscriptions() + assert.Equal(t, len(subscriptions), 2) +} + +func TestGetSubscriptionByIdSuccess(t *testing.T) { + NewResthook().FlushSubscriptions() + sub1 := CreateSubscription(models.EventTypeCreated, int64(5), int64(10), "http://localhost:8087/xapps_hook") + sub2 := CreateSubscription(models.EventTypeModified, int64(5), int64(10), "http://localhost:8087/xapps_hook2") + r1 := NewResthook().AddSubscription(sub1) + r2 := NewResthook().AddSubscription(sub2) + + resp1, ok := NewResthook().GetSubscriptionById(r1.ID) + assert.Equal(t, ok, true) + assert.Equal(t, resp1.Data, sub1.Data) + + resp2, ok := NewResthook().GetSubscriptionById(r2.ID) + assert.Equal(t, ok, true) + assert.Equal(t, resp2.Data, sub2.Data) +} + +func TestTeardown(t *testing.T) { + NewResthook().FlushSubscriptions() +} + +func CreateSubscription(et models.EventType, maxRetries, retryTimer int64, targetUrl string) models.SubscriptionRequest { + return models.SubscriptionRequest{&models.SubscriptionData{et, &maxRetries, &retryTimer, &targetUrl}} +} diff --git a/pkg/resthooks/types.go b/pkg/resthooks/types.go new file mode 100755 index 0000000..830576b --- /dev/null +++ b/pkg/resthooks/types.go @@ -0,0 +1,41 @@ +/* +================================================================================== + 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 resthooks + +import ( + sdl "gerrit.oran-osc.org/r/ric-plt/sdlgo" + cmap "github.com/orcaman/concurrent-map" + "net/http" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/models" +) + +type SubscriptionInfo struct { + Id string + req models.SubscriptionRequest + resp models.SubscriptionResponse +} + +type Resthook struct { + client *http.Client + subscriptions cmap.ConcurrentMap + db *sdl.SdlInstance + Seq int64 +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100755 index 0000000..8ea4d40 --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,68 @@ +/* +================================================================================== + 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 util + +import ( + "bytes" + "errors" + "github.com/spf13/viper" + "os" + "os/exec" + "strings" + "time" + + "gerrit.oran-osc.org/r/ric-plt/appmgr/pkg/appmgr" +) + +var execCommand = exec.Command + +func Exec(args string) (out []byte, err error) { + cmd := execCommand("/bin/sh", "-c", args) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + appmgr.Logger.Info("Running command: %s ", cmd.Args) + for i := 0; i < viper.GetInt("helm.retry"); i++ { + if err = cmd.Run(); err != nil { + appmgr.Logger.Error("Command failed: %v - %s, retrying", err.Error(), stderr.String()) + time.Sleep(time.Duration(2) * time.Second) + continue + } + break + } + + if err == nil && !strings.HasSuffix(os.Args[0], ".test") { + appmgr.Logger.Info("command success: %s", stdout.String()) + return stdout.Bytes(), nil + } + + return stdout.Bytes(), errors.New(stderr.String()) +} + +var HelmExec = func(args string) (out []byte, err error) { + return Exec(strings.Join([]string{"helm", args}, " ")) +} + +var KubectlExec = func(args string) (out []byte, err error) { + return Exec(strings.Join([]string{"kubectl", args}, " ")) +} diff --git a/scripts/appmgrcli b/scripts/appmgrcli index 22f0677..4b9637d 100755 --- a/scripts/appmgrcli +++ b/scripts/appmgrcli @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/perl -w # # Copyright (c) 2019 AT&T Intellectual Property. # Copyright (c) 2019 Nokia. @@ -19,26 +19,34 @@ ############################# # Simple cli for xapp manager # -# In addition to standard shell tools, requires packages "curl" and +# In addition to standard shell tools, requires basic Perl installation +# (Ubuntu package "perl-base", installed by default), packages "curl" and # "yajl-tools" (the second provides json_reformat on Ubuntu; on Red Hat-style # distributions install "yajl" instead). # -myname=appmgrcli +use strict; +use Getopt::Long; +use Fcntl; -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 - ;; - (config|upload) - cmd=config - ;; - (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` +if (exists $ENV{"APPMGR_HOST"}) { + $host=$ENV{"APPMGR_HOST"}; +} +if (exists $ENV{"APPMGR_PORT"}) { + $port=$ENV{"APPMGR_PORT"}; +} + +# Overrides for some deploy parameters + +my $configName = ""; +my $namespace = "ricxapp"; +my $releaseName = ""; +my $helmVersion = "0.0.1"; +my $overrideFile = ""; +my $podHost = ""; + +# The curl command can be overridden for testing with a dummy. + +my $curl = "curl"; + +Getopt::Long::Configure("no_auto_abbrev", "permute"); +if (! GetOptions("h=s" => \$host, + "p=i" => \$port, + "c=s" => \$curl, + "ConfigName=s" => \$configName, + "Namespace=s" => \$namespace, + "ReleaseName=s" => \$releaseName, + "HelmVersion=s" => \$helmVersion, + "OverrideFile=s" => \$overrideFile, + "podHost=s" => \$podHost, + "help" => \$showhelp, + "v" => \$verbose)) { + print "$myname: Error in options\n"; + helphint(); + exit 1; +} + +if ($showhelp) { + usage(); + exit 0; +} + +if ($verbose) { + print "host = $host\n"; + print "port = $port\n"; + print "ConfigName = $configName\n"; + print "Namespace = $namespace\n"; + print "ReleaseName = $releaseName\n"; + print "HelmVersion = $helmVersion\n"; + print "OverrideFile = $overrideFile\n"; + print "podHost = $podHost\n"; + for (my $idx = 0; $idx <= $#ARGV; ++$idx) { + print "\$ARGV[$idx] = $ARGV[$idx]\n"; + } +} + +# Verify command and call handler function + +my %commands = ( + "deploy" => \&do_deploy, + "dep" => \&do_deploy, + "undeploy" => \&do_undeploy, + "undep" => \&do_undeploy, + "status" => \&do_status, + "stat" => \&do_status, + "subscriptions" => \&do_subscriptions, + "subs" => \&do_subscriptions, + "health" => \&do_health, + "heal" => \&do_health, + "config" => \&do_config, + "help" => \&usage +); + +if ($#ARGV < 0) { + print "$myname: Missing command\n"; + helphint(); + exit 1; +} + # Variable status used for the return value of the whole script. -status=0 +my $status = 0; + +my $command = $ARGV[0]; +shift; +if (exists $commands{$command}) { + # Call the handler function with the rest of the command line + $commands{$command}(@ARGV); + exit $status; # Default exit. A handler can exit also if more convenient +} +print "$myname: Unrecognised command $command\n"; +helphint(); +exit 1; + +my $errfile; +my $resultfile; + + +sub make_temp_name($) { + my $tmpsuffix = "${$}${^T}"; + return "$_[0].$tmpsuffix"; +} + +sub make_temps { + $errfile = make_temp_name("/tmp/appmgr_e"); + $resultfile = make_temp_name("/tmp/appmgr_r"); +} + +sub remove_temps { + unlink ($errfile, $resultfile); +} + +sub print_file($$) { + my $outputhandle = $_[0]; + my $filename = $_[1]; + my $buffer; + my $inhandle; + if (!open($inhandle, "<", $filename)) { + print $outputhandle "$myname print_file: cannot open $filename: $!\n"; + return; + } + while (read($inhandle, $buffer, 4000) > 0) { + print $outputhandle $buffer; + } + close($inhandle); +} + +# The HTTP protocol result code, filled in by rest(). + +my $http_code = ""; + +# Helper: Given a curl output file, extract the number from ##code line. +# return ERROR if file cannot be opened, or "" if no code found. + +sub find_http_code($) { + my ($fh, $line, $code); + open($fh, "<", $_[0]) or return "ERROR"; + while ($line = <$fh>) { + if ($line =~ /^##([0-9]+)/) { + return $1; + } + } + return ""; +} # 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. +# returns true (1) if OK, and any returned data is in $resultfile +# else 0, and error message from curl is in $errfile, which is printed +# before returning the 0. # # 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. +# To get the http code, using -w with format. The result comes at the end +# of the output, so "decorating" it for easier filtering. +# The code is put to global $http_code. # -rest() { - local data - if [ "x$3" != "x" ]; then - data="--data $3" - fi +sub rest($$_) { + my $method = $_[0]; + my $path = $_[1]; + my $data = $_[2] || ""; + my $retval = 1; + my $http_status_file = make_temp_name("/tmp/appmgr_h"); + + # This redirects stderr (fd 2) to $errfile, but saving normal stderr + # so that if can be restored. + open(OLDERR, ">&", \*STDERR) or die "Can't dup STDERR: $!"; + open(ERRFILE, ">", $errfile) or die "open errorfile failed"; + open(STDERR, ">&", \*ERRFILE) or die "Can't dup ERRFILE: $!"; + + # This redirects stdout (fd 1) to $http_status_file, but saving original + # so that if can be restored. + open(OLDSTDOUT, ">&", \*STDOUT) or die "Can't dup STDOUT: $!"; + open(HTTP_STATUS_FILE, ">", $http_status_file) or die "open http status file failed"; + open(STDOUT, ">&", \*HTTP_STATUS_FILE) or die "Can't dup HTTP_STATUS_FILE: $!"; + + my @args = ($curl, "--silent", "--show-error", "--connect-timeout", "20", + "--header", "Content-Type: application/json", "-X", $method, + "-o", $resultfile, "-w", '\n##%{http_code}\n', + "http://${host}:${port}${path}"); + if ($data ne "") { + push(@args, "--data"); + push(@args, $data); + } + if ($verbose) { + print OLDSTDOUT "Running: " . join(" ", @args) . "\n"; + } + if (system(@args) == -1) { + print OLDSTDOUT "$myname: failed to execute @args\n"; + $retval = 0; + } + elsif ($? & 127) { + printf OLDSTDOUT "$myname: child died with signal %d, %s coredump\n", + ($? & 127), ($? & 128) ? 'with' : 'without'; + $retval = 0; + } + else { + my $curl_exit_code = $? >> 8; + if ($curl_exit_code == 0) { + seek HTTP_STATUS_FILE, 0, 0; # Ensures flushing + $http_code = find_http_code($http_status_file); + if ($http_code eq "ERROR") { + print OLDSTDOUT "$myname: failed to open temp file $http_status_file\n"; + $retval = 0; + } + elsif ($http_code eq "") { + print OLDSTDOUT "$myname: curl failed to provide HTTP code\n"; + $retval = 0; + } + else { + if ($verbose) { + print OLDSTDOUT "HTTP status code = $http_code\n"; + } + $retval = 1; # Interaction OK from REST point of view + } + } + else { + print_file(\*OLDSTDOUT, $errfile); + $retval = 0; + } + } + open(STDOUT, ">&", \*OLDSTDOUT) or die "Can't dup OLDSTDOUT: $!"; + open(STDERR, ">&", \*OLDERR) or die "Can't dup OLDERR: $!"; + unlink($http_status_file); + return $retval; +} + +# Pretty-print a JSON file to stdout. +# (currently uses json_reformat command) +# Skips the ##httpcode line we make "curl" +# add in order to get access to the HTTP status. - 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 +sub print_json($) { + my $filename = $_[0]; + my ($line, $inhandle, $outhandle); + if (!open($inhandle, "<", $filename)) { + print "$myname print_json: cannot open $filename: $!\n"; + return; + } + if (!open($outhandle, "|json_reformat")) { + print "$myname print_json: cannot pipe to json_reformat: $!\n"; + return; + } + while ($line = <$inhandle>) { + if (! ($line =~ /^##[0-9]+/)) { + print $outhandle $line; + } + } + close($outhandle); + close($inhandle); } -remove_temps () { - rm -f $errfile $resultfile +# Append an entry like ","name":"value" to the first parameter, if "name" +# names a variable with non-empty value. +# Else returns the unmodified first parameter. + +sub append_option($$) { + my $result = $_[0]; + my $var = $_[1]; + my $val = eval("\$$var"); + if ($val ne "") { + $result = "$result,\"$var\":\"$val\""; + } + return $result; } -# Execute command ($cmd guaranteed to be valid) +# Command handlers # 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 -base_config=$base/config - -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 appmgr 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 +# Functions for each command below + +# Deploy: +# The deploy command has one mandatory parameter "name" in the API, +# and several optional ones. Used mainly internally for testing, because +# they all override Helm chart values: +# "helmVersion": Helm chart version to be used +# "releaseName": The releas name of xApp visible in K8s +# "namespace": Name of the namespace to which xApp is deployed. +# "overrideFile": The file content used to override values.yaml file +# this host from the host the xapp manager is running in, we use the term +# and variable name "podHost" here. +# The options come from options (see GetOptions() call). + +sub do_deploy(@) { + my $name = $_[0] || ""; + if ($name ne "") { + my $data = "{\"XappName\":\"$name\""; + $data = append_option($data, "helmVersion"); + $data = append_option($data, "releaseName"); + $data = append_option($data, "namespace"); + $data = append_option($data, "overrideFile"); + $data = $data . "}"; + make_temps(); + if (rest("POST", $base_xapps, $data)) { + if ($http_code eq "201") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID PARAMETERS SUPPLIED"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + print "$myname: Error: expected the name of xapp to deploy\n"; + $status = 1; + } +} + +sub do_undeploy(@) { + my $name = $_[0] || ""; + my $urlpath = $base_xapps; + if ($name ne "") { + make_temps(); + $urlpath = "$urlpath/$name"; + if (rest("DELETE", $urlpath)) { + if ($http_code eq "204") { + print "SUCCESSFUL DELETION\n"; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID XAPP NAME SUPPLIED"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status = 1; + } + remove_temps(); + } + else { + print "$myname: Error: expected the name of xapp to undeploy\n"; + $status = 1; + } +} +sub do_status(@) { + my $name = $_[0] || ""; + my $instance = $_[1] || ""; + my $urlpath = $base_xapps; + + if ($name ne "") { + $urlpath = "$urlpath/$name"; + } + if ($instance ne "") { + $urlpath = "$urlpath/instances/$instance" + } + make_temps(); + if (rest("GET", $urlpath)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID XAPP NAME SUPPLIED"; + } + if ($http_code eq "404") { + $error = "XAPP NOT FOUND"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status = 1; + } + remove_temps(); +} + +# Helpers for subscription: # 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. +# If errors, returns false (0) and prints errors, else returns 1. # -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 appmgr 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 -} - -do_config() { - local urlpath - urlpath=$base_config - case $1 in - (get|list) - if [ "x$2" != "x" ]; then - urlpath="$urlpath/$2" - fi - if rest GET $urlpath; then - json_reformat < $resultfile - else - status=1 - fi - ;; - (add|update) - if rest POST $urlpath "@$2" ; then - cat $resultfile - else - status=1 - fi - ;; - (del|delete|remove|rem) - if rest DELETE $urlpath "@$2" ; then - cat $resultfile - else - status=1 - fi - ;; - (*) - echo "$myname: unrecognized config 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" - ;; - (config) - do_config "$2" "$3" - ;; - (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: +sub validate_subscription(@) { + # Using the API parameter names + my $targetUrl = $_[0] || ""; + my $eventType = $_[1] || ""; + my $maxRetries = $_[2] || ""; + my $retryTimer = $_[3] || ""; + my $retval = 1; + + if (! ($targetUrl =~ /^http:\/\/.*/ or $targetUrl =~ /^https:\/\/.*/)) { + print "$myname: bad URL $targetUrl\n"; + $retval = 0; + } + if ($eventType ne "created" and $eventType ne "deleted" and + $eventType ne "all") { + print "$myname: unrecognized event $eventType\n"; + $retval = 0; + } + if (! ($maxRetries =~ /^[0-9]+$/)) { + print "$myname: invalid maximum retries count $maxRetries\n"; + $retval = 0; + } + if (! ($retryTimer =~ /^[0-9]+$/)) { + print "$myname: invalid retry time $retryTimer\n"; + $retval = 0; + } + return $retval; +} + +# Format a subscriptionRequest JSON object + +sub make_subscriptionRequest(@) { + my $targetUrl = $_[0]; + my $eventType = $_[1]; + my $maxRetries = $_[2]; + my $retryTimer = $_[3]; + return "{\"Data\": {\"TargetUrl\":\"$targetUrl\",\"EventType\":\"$eventType\",\"MaxRetries\":$maxRetries,\"RetryTimer\":$retryTimer}}"; +} + +# Subscriptions: +# $1 is sub-command: list, add, delete, modify + +sub do_subscriptions(@) { + my $subcommand = $_[0] || ""; + shift; + + my %subcommands = ( + "list" => \&do_subscription_list, + "add" => \&do_subscription_add, + "delete" => \&do_subscription_delete, + "del" => \&do_subscription_delete, + "modify" => \&do_subscription_modify, + "mod" => \&do_subscription_modify + ); + if (exists $subcommands{$subcommand}) { + $subcommands{$subcommand}(@_); + } + else { + print "$myname: unrecognized subscriptions subcommand $subcommand\n"; + helphint(); + $status=1 + } +} + +# list: With empty parameter, list all, else the parameter is +# a subscriptionId + +sub do_subscription_list(@) { + my $urlpath=$base_subs; + my $subscriptionId = $_[0] || ""; + if ($subscriptionId ne "") { + $urlpath = "$urlpath/$subscriptionId"; + } + make_temps(); + if (rest("GET", $urlpath)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID SUBSCRIPTION ID $subscriptionId"; + } + elsif ($http_code eq "404") { + $error = "SUBSCRIPTION $subscriptionId NOT FOUND"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); +} + +sub do_subscription_add(@) { + my $urlpath=$base_subs; + + if (validate_subscription(@_)) { + make_temps(); + if (rest("POST", $urlpath, make_subscriptionRequest(@_))) { + if ($http_code eq "201") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +sub do_subscription_delete(@) { + my $urlpath=$base_subs; + my $subscriptionId = $_[0] || ""; + if ($subscriptionId ne "") { + $urlpath = "$urlpath/$subscriptionId"; + } + else { + print "$myname: delete: Subscription id required\n"; + $status=1; + return; + } + make_temps(); + if (rest("DELETE", $urlpath)) { + if ($http_code eq "204") { + print "SUBSCRIPTION $subscriptionId DELETED\n"; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID SUBSCRIPTION ID $subscriptionId"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status = 1; + } + remove_temps(); +} + +sub do_subscription_modify(@) { + my $urlpath=$base_subs; + if (defined $_[0]) { + $urlpath = "$urlpath/$_[0]"; + } + else { + print "$myname: modify: Subscription id required\n"; + $status=1; + return; + } + shift; + if (validate_subscription(@_)) { + make_temps(); + if (rest("PUT", $urlpath, make_subscriptionRequest(@_))) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +sub do_health(@) { + my $urlpath=$base_health; + my $check = $_[0] || ""; + # API now defines two types of checks, either of + # which must be specified. + if ($check ne "alive" and $check ne "ready") { + print "$myname: health check type required (alive or ready)\n"; + $status=1; + return; + } + $urlpath = "$urlpath/$check"; + make_temps(); + if (rest("GET", $urlpath)) { + my $res; + if ($check eq "alive") { + # If GET succeeds at all, the xapp manager is alive, no + # need to check the HTTP code. + $res = "ALIVE"; + } + else { + if ($http_code eq "200") { + $res = "READY"; + } + elsif ($http_code eq "503") { + $res = "NOT READY"; + } + elsif ($http_code eq "500") { + $res = "INTERNAL ERROR"; + } + else { + $res = "UNKNOWN STATUS $http_code"; + } + } + print "$res\n"; + } + else { + $status = 1; + print "$myname: health check failed to contact appmgr\n"; + } + remove_temps(); +} + +sub do_config(@) { + my $subcommand = $_[0] || ""; + shift; + + my %subcommands = ( + "list" => \&do_config_list, + "add" => \&do_config_add, + "delete" => \&do_config_delete, + "del" => \&do_config_delete, + "modify" => \&do_config_modify, + "mod" => \&do_config_modify + ); + if (exists $subcommands{$subcommand}) { + $subcommands{$subcommand}(@_); + } + else { + print "$myname: unrecognized config subcommand $subcommand\n"; + helphint(); + $status=1 + } +} + +sub do_config_list(@) { + if (defined $_[0]) { + print "$myname: \"config list\" has no parameters\n"; + $status = 1; + return; + } + make_temps(); + if (rest("GET", $base_config)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); +} + +# validate_config() checks configuration commmand line. +# "config add" and "config modify" expect either single parameter which +# must be a JSON file that contains the whole thing to send (see API), +# or 5 parameters, where the first three are +# $_[0] = name +# $_[1] = configName (name of the configMap) +# $_[2] = namespace +# Followed by two file names: +# $_[3] = file containing configSchema +# $_[4] = file containing data for configMap +# Giving the last two literally on the command line does not make much sense, +# since they are arbitrary JSON data. +# On success, returns parameter count (1 or 5), depending on which kind of +# command line found. +# 0 if errors. + +# Check only the 3 names at the beginning of config add/modify/delete +sub validate_config_names(@) { + my $retval = 1; + # Names in the Kubernetes world consist of lowercase alphanumerics + # and - and . as specified in + # https://kubernetes.io/docs/concepts/overview/working-with-objects/name + for (my $idx = 0; $idx <= 2; ++$idx) { + if (! ($_[$idx] =~ /^[a-z][-a-z0-9.]*$/)) { + print "$myname: invalid characters in name $_[$idx]\n"; + $retval = 0; + } + } + return $retval; +} + +sub validate_config(@) { + my $retval = 1; + print "validate_config args @_\n"; + if ($#_ == 0) { + if (! -r $_[0]) { + print "$myname: config file $_[0] cannot be read: $!\n"; + $retval = 0; + } + } + elsif ($#_ == 4) { + $retval = 5; + if (! validate_config_names(@_)) { + $retval = 0; + } + for (my $idx = 3; $idx <= 4; ++$idx) { + if (! -r $_[$idx]) { + print "$myname: cannot read file $_[$idx]\n"; + $retval = 0; + } + } + } + else { + print "$myname: config add: 1 or 5 parameter expected\n"; + $retval = 0; + } + return $retval; +} + +# Generate JSON for the xAppConfig element (see API). + +sub make_xAppConfigInfo($$$) { + return "{\"xAppName\":\"$_[0]\",\"configMapName\":\"$_[1]\",\"namespace\":\"$_[2]\"}"; +} + +sub make_xAppConfig(@) { + my $retval = "{\"xAppConfigInfo\":" . make_xAppConfigInfo($_[0],$_[1],$_[2]); + my $fh; + open($fh, "<", $_[3]) or die "failed to open $_[3]"; + my @obj = <$fh>; + close($fh); + $retval = $retval . ",\"configSchema\":" . join("", @obj); + open($fh, "<", $_[4]) or die "failed to open $_[4]"; + @obj = <$fh>; + close($fh); + $retval = $retval . ",\"configMap\":" . join("", @obj) . "}"; +} + +sub do_config_add(@) { + my $paramCount; + + $paramCount = validate_config(@_); + if ($paramCount > 0) { + my $xAppConfig; + if ($paramCount == 1) { + $xAppConfig = "\@$_[0]"; + } + else { + $xAppConfig = make_xAppConfig(@_); + } + make_temps(); + if (rest("POST", $base_config, $xAppConfig)) { + if ($http_code eq "201") { + print_json $resultfile; + $status = 0; + } + elsif ($http_code eq "422") { # Validation failed, details in result + print_json $resultfile; + $status = 1; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +sub do_config_modify(@) { + my $paramCount; + + $paramCount = validate_config(@_); + if ($paramCount > 0) { + my $xAppConfig; + if ($paramCount == 1) { + $xAppConfig = "\@$_[0]"; + } + else { + $xAppConfig = make_xAppConfig(@_); + } + make_temps(); + if (rest("PUT", $base_config, $xAppConfig)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + elsif ($http_code eq "422") { # Validation failed, details in result + print_json $resultfile; + $status = 1; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +# In config delete, allow either 1 parameter naming a file that contains +# a JSON xAppConfigInfo object, or 3 parameters giving the +# components (xAppName, configMapName, namespace), same as +# in add and modify operations. + +sub do_config_delete(@) { + my $xAppConfigInfo = ""; + + if ($#_ != 0 and $#_ != 2) { + print "$myname: wrong number of parameters for config delete\n"; + $status = 1; + } + elsif ($#_ == 0) { + if (-r $_[0]) { + $xAppConfigInfo = "\@$_[0]"; + } + else { + print "$myname: config file $_[0] cannot be read: $!\n"; + $status = 1; + } + } + elsif (($#_ == 2) && validate_config_names(@_)) { + $xAppConfigInfo = make_xAppConfigInfo($_[0],$_[1],$_[2]); + } + else { + print "$myname: bad parameters for config delete\n"; + $status = 1; + } + if ($xAppConfigInfo ne "") { + make_temps(); + if (rest("DELETE", $base_config, $xAppConfigInfo)) { + if ($http_code eq "204") { + print "SUCCESFUL DELETION OF CONFIG\n"; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID PARAMETERS SUPPLIED"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } +} -- 2.16.6