Swagger-baser server REST API interface 34/1634/1 v0.2.0
authorMohamed Abukar <abukar.mohamed@nokia.com>
Wed, 13 Nov 2019 15:57:15 +0000 (17:57 +0200)
committerMohamed Abukar <abukar.mohamed@nokia.com>
Sat, 16 Nov 2019 15:40:54 +0000 (17:40 +0200)
Change-Id: Ic2057a93bd9dfbb19c3b00cc497a4243120c6662
Signed-off-by: Mohamed Abukar <abukar.mohamed@nokia.com>
27 files changed:
Dockerfile [changed mode: 0644->0755]
api/appmgr_rest_api.json [deleted file]
api/appmgr_rest_api.yaml
build/make.go.mk [changed mode: 0644->0755]
cmd/appmgr.go [moved from cmd/appmgr/main.go with 83% similarity]
cmd/appmgr/api_test.go [deleted file]
cmd/appmgr/db.go [deleted file]
cmd/appmgr/subscriptions.go [deleted file]
cmd/appmgr/subscriptions_test.go [deleted file]
cmd/appmgr/types.go [deleted file]
container-tag.yaml [changed mode: 0644->0755]
go.mod
go.sum
pkg/appmgr/appmgr.go [moved from cmd/appmgr/config.go with 75% similarity]
pkg/appmgr/types.go [new file with mode: 0755]
pkg/cm/cm.go [moved from cmd/appmgr/desc.go with 51% similarity]
pkg/cm/cm_test.go [moved from cmd/appmgr/desc_test.go with 59% similarity]
pkg/helm/helm.go [moved from cmd/appmgr/helm.go with 57% similarity]
pkg/helm/helm_test.go [moved from cmd/appmgr/helm_test.go with 53% similarity]
pkg/logger/logger.go [moved from cmd/appmgr/logger.go with 87% similarity]
pkg/restful/restful.go [new file with mode: 0755]
pkg/restful/types.go [new file with mode: 0755]
pkg/resthooks/resthooks.go [new file with mode: 0755]
pkg/resthooks/resthooks_test.go [new file with mode: 0755]
pkg/resthooks/types.go [new file with mode: 0755]
pkg/util/util.go [new file with mode: 0755]
scripts/appmgrcli

old mode 100644 (file)
new mode 100755 (executable)
index adc6a04..027e372
@@ -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 (file)
index 2b08195..0000000
+++ /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"
-        }
-      }
-    }
-  }
-}
index 9e7c15b..04f4f0a 100644 (file)
@@ -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'
old mode 100644 (file)
new mode 100755 (executable)
index 286d9b1..2208487
@@ -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
 
similarity index 83%
rename from cmd/appmgr/main.go
rename to cmd/appmgr.go
index 8788405..b658367 100755 (executable)
 
 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 (executable)
index 248ffee..0000000
+++ /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 (executable)
index 97865c2..0000000
+++ /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 (executable)
index 53bc212..0000000
+++ /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 (executable)
index 5d79d00..0000000
+++ /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 (executable)
index 1c9556d..0000000
+++ /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
-)
old mode 100644 (file)
new mode 100755 (executable)
index a704888..3724441
@@ -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 (file)
--- 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 (file)
--- 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=
similarity index 75%
rename from cmd/appmgr/config.go
rename to pkg/appmgr/appmgr.go
index 5e1975e..28e98f9 100755 (executable)
 ==================================================================================
 */
 
-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 (executable)
index 0000000..afdb0b4
--- /dev/null
@@ -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
+)
similarity index 51%
rename from cmd/appmgr/desc.go
rename to pkg/cm/cm.go
index 8cc39a5..af5372d 100755 (executable)
@@ -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
        }
similarity index 59%
rename from cmd/appmgr/desc_test.go
rename to pkg/cm/cm_test.go
index 8f005a1..be600bb 100755 (executable)
 ==================================================================================
 */
 
-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)
 }
similarity index 57%
rename from cmd/appmgr/helm.go
rename to pkg/helm/helm.go
index 8a8154a..a3d7b18 100755 (executable)
 ==================================================================================
 */
 
-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-<namespace>-<chartname>-rmr.<namespace>"
-       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)
 }
similarity index 53%
rename from cmd/appmgr/helm_test.go
rename to pkg/helm/helm_test.go
index 6ad10c6..fb72358 100755 (executable)
 ==================================================================================
 */
 
-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
+}
similarity index 87%
rename from cmd/appmgr/logger.go
rename to pkg/logger/logger.go
index d1bcf15..251e90f 100755 (executable)
 ==================================================================================
 */
 
-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 (executable)
index 0000000..c0ef580
--- /dev/null
@@ -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 (executable)
index 0000000..189962d
--- /dev/null
@@ -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 (executable)
index 0000000..5076b06
--- /dev/null
@@ -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 (executable)
index 0000000..018dee8
--- /dev/null
@@ -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 (executable)
index 0000000..830576b
--- /dev/null
@@ -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 (executable)
index 0000000..8ea4d40
--- /dev/null
@@ -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}, " "))
+}
index 22f0677..4b9637d 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/perl -w
 #
 # Copyright (c) 2019 AT&T Intellectual Property.
 # Copyright (c) 2019 Nokia.
 #############################
 # 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 <<EOF1
-usage: $myname [-h host] [-p port] [-v] command params...
-- command is one of deploy, undeploy, status, subscriptions, health, help
+my $myname="appmgrcli";
+
+sub usage {
+    print <<"EOF1";
+usage: $myname [-h host] [-p port] [-v] [-c curlprog] command params...
+- command is deploy, undeploy, status, subscriptions, health, config, help
 - (abbreviations dep, undep, stat, subs, heal allowed)
 - Parameters of the commands that may have parameters:
 -- deploy: name of the xapp to deploy
+---- Deployment parameters come from the Help chart but the following can be
+---- overridden by option with the same name (for example --configName=cname):
+----- helmVersion ReleaseName namespace podHost (host of pods).
 -- undeploy: name of the xapp to undeploy
 -- status:
 ---- No parameters: Lists information about all deployed xapps
 ---- xapp name as parameter: Prints information about the given xapp
 ---- xapp name and instance: Lists information about the given instance only
 -- subscriptions is followed by sub-command list, add, delete, or modify
----(abbreviations del and mod for delete and modify are allowed):
+--- (abbreviations del and mod for delete and modify are allowed):
 ---- list without parameters lists all subscriptions
 ---- list with subscription id prints that subscription
 ---- add URL eventType maxRetry retryTimer
@@ -50,356 +58,997 @@ usage: $myname [-h host] [-p port] [-v] command params...
 --------the rest of the parameters are like in add
 ---- delete id
 ------- id is the subscription id to delete (find out with the list command)
+-- config is followed by sub-command list, add, delete, or modify
+--- (abbreviations del and mod for delete and modify are allowed):
+---- list (no pars)
+------ lists the configuration of all xapps
+---- add jsonfile
+------ Creates xapp configuration. the jsonfile must contain all data (see API)
+---- add name configName namespace configSchemaFile configDataFile
+------ Creates xapp configuration, but unlike in the 1-parameter form,
+------ Xapp name, config map name and namespace are separate parameters.
+------ The other data come from JSON files.
+---- modify
+------ Modifies existing configuration. Same parameters (1 or 5) as in add.
+---- delete name configName namespace
+------ Deletes the configuration identified by the parameters.
+--------------------------------------------------------------
 - Default values for host and port can be set in environment
 - variables APPMGR_HOST and APPMGR_PORT
 - Option -v sets verbose mode.
+- Option -c overrides the used curl program name ("curl" by default).
+- Exit code is 0 for success, 1 for any kind of failure.
 EOF1
+    exit 0;
+}
+
+sub helphint {
+    print "run $myname help (or --help) for instructions\n";
 }
 
 # Defaults
 
-host=localhost
-port=8080
-verbose=0
+my $host="localhost";
+my $port=8080;
+my $verbose=0;
+my $showhelp = 0;
+
+# API URLs
+
+my $base="/ric/v1";
+my $base_xapps="$base/xapps";
+my $base_health="$base/health";
+my $base_subs="$base/subscriptions";
+my $base_config="$base/config";
 
 # Check for environment override
-if [ "x$APPMGR_HOST" != "x" ]; then
-    host="$APPMGR_HOST"
-fi
-if [ "x$APPMGR_PORT" != "x" ]; then
-    port="$APPMGR_PORT"
-fi
-
-# Proper shell option parsing:
-while getopts  "h:p:v" flag
-do
-  # Curiously, getopts does not guard against an argument-requiring option
-  # eating the next option. It also does not handle the -- convention.
-  # Here is how to fix that.
-  if [ "X$OPTARG" = 'X--' ]; then
-    break # Explicit end of options
-  fi
-  if expr -- "$OPTARG" : '-.*' > /dev/null ; then
-    echo $myname: Option -$flag has no required value, or value begins with -,
-    echo - which is disallowed.
-    usage
-    exit 1
-  fi
-  case $flag in
-  (h) host="$OPTARG"
-      ;;
-  (p) port="$OPTARG"
-      ;;
-  (v) verbose=1
-      ;;
-  (*)
-      echo $myname: Bad option letter or required option argument missing.
-      usage
-      exit 1
-      ;;
-  esac
-done
-# Get rid of the option part
-shift $((OPTIND-1))
-
-if [ $verbose = 1 ]; then
-  echo "host = $host"
-  echo "port = $port"
-fi
-
-# Verify command
-
-case $1 in
-  (deploy|dep)
-    cmd=deploy
-    ;;
-  (undeploy|undep)
-    cmd=undeploy
-    ;;
-  (status|stat)
-    cmd=status
-    ;;
-  (subscriptions|subs)
-    cmd=subscriptions
-    ;;
-  (health|heal)
-    cmd=health
-    ;;
-  (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();
+    }
+}