Add apps under composite app API 76/102176/7
authorSrivahni Chivukula <srivahni.chivukula@intel.com>
Fri, 21 Feb 2020 18:08:04 +0000 (10:08 -0800)
committerSrivahni Chivukula <srivahni.chivukula@intel.com>
Thu, 12 Mar 2020 19:01:25 +0000 (19:01 +0000)
Implemented create, get and delete
handlers for the apps under composite
app.

Added unit tests

Added remove function to mockdb

Handled multipart POST request to
upload file along with app data.

Issue-ID: MULTICLOUD-998
Signed-off-by: Srivahni Chivukula <srivahni.chivukula@intel.com>
Change-Id: I25c1faba1212c0cc881c2cd599e8e66a7b93033e

13 files changed:
src/orchestrator/api/api.go
src/orchestrator/api/apphandler.go [new file with mode: 0644]
src/orchestrator/api/clusterhandler_test.go
src/orchestrator/api/composite_app_handler.go [moved from src/orchestrator/api/compositeapphandler.go with 99% similarity]
src/orchestrator/api/composite_profilehandler_test.go
src/orchestrator/api/controllerhandler_test.go
src/orchestrator/api/projecthandler_test.go
src/orchestrator/cmd/main.go
src/orchestrator/pkg/infra/db/mock.go
src/orchestrator/pkg/module/app.go [new file with mode: 0644]
src/orchestrator/pkg/module/app_test.go [new file with mode: 0644]
src/orchestrator/pkg/module/compositeapp.go
src/orchestrator/pkg/module/module.go

index 9b33daf..70b40d9 100644 (file)
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package api
 
 import (
@@ -25,6 +26,7 @@ var moduleClient *moduleLib.Client
 // NewRouter creates a router that registers the various urls that are supported
 func NewRouter(projectClient moduleLib.ProjectManager,
        compositeAppClient moduleLib.CompositeAppManager,
+       appClient moduleLib.AppManager,
        ControllerClient moduleLib.ControllerManager,
        clusterClient moduleLib.ClusterManager,
        genericPlacementIntentClient moduleLib.GenericPlacementIntentManager,
@@ -73,6 +75,17 @@ func NewRouter(projectClient moduleLib.ProjectManager,
        router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", compAppHandler.getHandler).Methods("GET")
        router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", compAppHandler.deleteHandler).Methods("DELETE")
 
+       if appClient == nil {
+               appClient = moduleClient.App
+       }
+       appHandler := appHandler{
+               client: appClient,
+       }
+
+       router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps", appHandler.createAppHandler).Methods("POST")
+       router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps/{app-name}", appHandler.getAppHandler).Methods("GET")
+       router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps/{app-name}", appHandler.deleteAppHandler).Methods("DELETE")
+
        if compositeProfileClient == nil {
                compositeProfileClient = moduleClient.CompositeProfile
        }
diff --git a/src/orchestrator/api/apphandler.go b/src/orchestrator/api/apphandler.go
new file mode 100644 (file)
index 0000000..3cd2dbc
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020 Intel Corporation, Inc
+ *
+ * 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 api
+
+import (
+       "bytes"
+       "encoding/base64"
+       "encoding/json"
+       "io"
+       "io/ioutil"
+       "mime"
+       "mime/multipart"
+       "net/http"
+       "net/textproto"
+
+       moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module"
+       "github.com/onap/multicloud-k8s/src/orchestrator/utils"
+
+       "github.com/gorilla/mux"
+)
+
+// appHandler to store backend implementations objects
+// Also simplifies mocking for unit testing purposes
+type appHandler struct {
+       // Interface that implements App operations
+       // We will set this variable with a mock interface for testing
+       client moduleLib.AppManager
+}
+
+// createAppHandler handles creation of the App entry in the database
+// This is a multipart handler. See following example curl request
+// curl -X POST http://localhost:9015/v2/projects/sampleProject/composite-apps/sampleCompositeApp/v1/apps \
+// -F "metadata={\"metadata\":{\"name\":\"app\",\"description\":\"sample app\",\"UserData1\":\"data1\",\"UserData2\":\"data2\"}};type=application/json" \
+// -F file=@/pathToFile
+
+func (h appHandler) createAppHandler(w http.ResponseWriter, r *http.Request) {
+       var a moduleLib.App
+       var ac moduleLib.AppContent
+
+       // Implemenation using multipart form
+       // Set Max size to 16mb here
+       err := r.ParseMultipartForm(16777216)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusUnprocessableEntity)
+               return
+       }
+
+       jsn := bytes.NewBuffer([]byte(r.FormValue("metadata")))
+       err = json.NewDecoder(jsn).Decode(&a)
+       switch {
+       case err == io.EOF:
+               http.Error(w, "Empty body", http.StatusBadRequest)
+               return
+       case err != nil:
+               http.Error(w, err.Error(), http.StatusUnprocessableEntity)
+               return
+       }
+
+       // Name is required.
+       if a.Metadata.Name == "" {
+               http.Error(w, "Missing name in POST request", http.StatusBadRequest)
+               return
+       }
+
+       //Read the file section and ignore the header
+       file, _, err := r.FormFile("file")
+       if err != nil {
+               http.Error(w, "Unable to process file", http.StatusUnprocessableEntity)
+               return
+       }
+
+       defer file.Close()
+
+       //Convert the file content to base64 for storage
+       content, err := ioutil.ReadAll(file)
+       if err != nil {
+               http.Error(w, "Unable to read file", http.StatusUnprocessableEntity)
+               return
+       }
+
+       err = utils.IsTarGz(bytes.NewBuffer(content))
+       if err != nil {
+               http.Error(w, "Error in file format", http.StatusUnprocessableEntity)
+               return
+       }
+
+       ac.FileContent = base64.StdEncoding.EncodeToString(content)
+
+       vars := mux.Vars(r)
+       projectName := vars["project-name"]
+       compositeAppName := vars["composite-app-name"]
+       compositeAppVersion := vars["version"]
+
+       ret, err := h.client.CreateApp(a, ac, projectName, compositeAppName, compositeAppVersion)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusInternalServerError)
+               return
+       }
+
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(http.StatusCreated)
+       err = json.NewEncoder(w).Encode(ret)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusInternalServerError)
+               return
+       }
+}
+
+// getAppHandler handles GET operations on a particular App Name
+// Returns an app
+func (h appHandler) getAppHandler(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+       projectName := vars["project-name"]
+       compositeAppName := vars["composite-app-name"]
+       compositeAppVersion := vars["version"]
+       name := vars["app-name"]
+
+       accepted, _, err := mime.ParseMediaType(r.Header.Get("Accept"))
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusNotAcceptable)
+               return
+       }
+
+       var retApp moduleLib.App
+       var retAppContent moduleLib.AppContent
+
+       retApp, err = h.client.GetApp(name, projectName, compositeAppName, compositeAppVersion)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusInternalServerError)
+               return
+       }
+
+       retAppContent, err = h.client.GetAppContent(name, projectName, compositeAppName, compositeAppVersion)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusInternalServerError)
+               return
+       }
+
+       switch accepted {
+       case "multipart/form-data":
+               mpw := multipart.NewWriter(w)
+               w.Header().Set("Content-Type", mpw.FormDataContentType())
+               w.WriteHeader(http.StatusOK)
+               pw, err := mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/json"}, "Content-Disposition": {"form-data; name=metadata"}})
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               if err := json.NewEncoder(pw).Encode(retApp); err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               pw, err = mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/octet-stream"}, "Content-Disposition": {"form-data; name=file; filename=fileContent"}})
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               fcBytes, err := base64.StdEncoding.DecodeString(retAppContent.FileContent)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               _, err = pw.Write(fcBytes)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+       case "application/json":
+               w.Header().Set("Content-Type", "application/json")
+               w.WriteHeader(http.StatusOK)
+               err = json.NewEncoder(w).Encode(retApp)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+       case "application/octet-stream":
+               w.Header().Set("Content-Type", "application/octet-stream")
+               w.WriteHeader(http.StatusOK)
+               fcBytes, err := base64.StdEncoding.DecodeString(retAppContent.FileContent)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               _, err = w.Write(fcBytes)
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+       default:
+               http.Error(w, "set Accept: multipart/form-data, application/json or application/octet-stream", http.StatusMultipleChoices)
+               return
+       }
+}
+
+// deleteAppHandler handles DELETE operations on a particular App Name
+func (h appHandler) deleteAppHandler(w http.ResponseWriter, r *http.Request) {
+       vars := mux.Vars(r)
+       projectName := vars["project-name"]
+       compositeAppName := vars["composite-app-name"]
+       compositeAppVersion := vars["version"]
+       name := vars["app-name"]
+
+       err := h.client.DeleteApp(name, projectName, compositeAppName, compositeAppVersion)
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusInternalServerError)
+               return
+       }
+
+       w.WriteHeader(http.StatusNoContent)
+}
index 71afdd1..e5161a4 100644 (file)
@@ -229,7 +229,7 @@ func TestClusterProviderCreateHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("POST", "/v2/cluster-providers", testCase.reader)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -307,7 +307,7 @@ func TestClusterProviderGetAllHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers", nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -377,7 +377,7 @@ func TestClusterProviderGetHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -426,7 +426,7 @@ func TestClusterProviderDeleteHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("DELETE", "/v2/cluster-providers/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -538,7 +538,7 @@ of clusterTest
 
                        request := httptest.NewRequest("POST", "/v2/cluster-providers/clusterProvider1/clusters", bytes.NewBuffer(body.Bytes()))
                        request.Header.Set("Content-Type", multiwr.FormDataContentType())
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -625,7 +625,7 @@ func TestClusterGetAllHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvder1/clusters", nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -706,7 +706,7 @@ func TestClusterGetHandler(t *testing.T) {
                        if len(testCase.accept) > 0 {
                                request.Header.Set("Accept", testCase.accept)
                        }
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -784,7 +784,7 @@ of clusterTest
                        if len(testCase.accept) > 0 {
                                request.Header.Set("Accept", testCase.accept)
                        }
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -834,7 +834,7 @@ func TestClusterDeleteHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("DELETE", "/v2/cluster-providers/clusterProvider1/clusters/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -880,7 +880,7 @@ func TestClusterLabelCreateHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("POST", "/v2/cluster-providers/cp1/clusters/cl1/labels", testCase.reader)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -944,7 +944,7 @@ func TestClusterLabelsGetHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers/cp1/clusters/cl1/labels", nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -1004,7 +1004,7 @@ func TestClusterLabelGetHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvider1/clusters/cl1/labels/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -1053,7 +1053,7 @@ func TestClusterLabelDeleteHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("DELETE", "/v2/cluster-providers/cp1/clusters/cl1/labels/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -1144,7 +1144,7 @@ func TestClusterKvPairsCreateHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("POST", "/v2/cluster-providers/cp1/clusters/cl1/kv-pairs", testCase.reader)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -1262,7 +1262,7 @@ func TestClusterKvPairsGetAllHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers/cp1/clusters/cl1/kv-pairs", nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -1352,7 +1352,7 @@ func TestClusterKvPairsGetHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvider1/clusters/cl1/kv-pairs/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -1401,7 +1401,7 @@ func TestClusterKvPairsDeleteHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("DELETE", "/v2/cluster-providers/cp1/clusters/cl1/kv-pairs/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
similarity index 99%
rename from src/orchestrator/api/compositeapphandler.go
rename to src/orchestrator/api/composite_app_handler.go
index 42c72cd..b54c488 100644 (file)
@@ -35,7 +35,6 @@ type compositeAppHandler struct {
 }
 
 // createHandler handles creation of the CompositeApp entry in the database
-// This is a multipart handler
 func (h compositeAppHandler) createHandler(w http.ResponseWriter, r *http.Request) {
        var c moduleLib.CompositeApp
 
index 360653c..7c84f12 100644 (file)
@@ -128,7 +128,7 @@ func Test_compositeProfileHandler_createHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("POST", "/v2/projects/{project-name}/composite-apps/{composite-app-name}/{version}/composite-profiles", testCase.reader)
-                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, testCase.cProfClient, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, testCase.cProfClient, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
index ab0aeed..3c543cb 100644 (file)
@@ -110,7 +110,7 @@ func TestControllerCreateHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("POST", "/v2/controllers", testCase.reader)
-                       resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -173,7 +173,7 @@ func TestControllerGetHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/controllers/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -222,7 +222,7 @@ func TestControllerDeleteHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("DELETE", "/v2/controllers/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
index af40f96..5e820aa 100644 (file)
@@ -119,7 +119,7 @@ func TestProjectCreateHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("POST", "/v2/projects", testCase.reader)
-                       resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -188,7 +188,7 @@ func TestProjectGetHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("GET", "/v2/projects/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
@@ -237,7 +237,7 @@ func TestProjectDeleteHandler(t *testing.T) {
        for _, testCase := range testCases {
                t.Run(testCase.label, func(t *testing.T) {
                        request := httptest.NewRequest("DELETE", "/v2/projects/"+testCase.name, nil)
-                       resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil))
+                       resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil))
 
                        //Check returned code
                        if resp.StatusCode != testCase.expectedCode {
index f95c057..001903a 100644 (file)
@@ -47,7 +47,7 @@ func main() {
                log.Fatalln("Exiting...")
        }
 
-       httpRouter := api.NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
+       httpRouter := api.NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
        loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter)
        log.Println("Starting Kubernetes Multicloud API")
 
index 79366d1..e43be8f 100644 (file)
@@ -96,3 +96,7 @@ func (m *MockDB) Find(table string, key Key, tag string) ([][]byte, error) {
 func (m *MockDB) Delete(table string, key Key, tag string) error {
        return m.Err
 }
+
+func (m *MockDB) Remove(table string, key Key) error {
+       return m.Err
+}
diff --git a/src/orchestrator/pkg/module/app.go b/src/orchestrator/pkg/module/app.go
new file mode 100644 (file)
index 0000000..c25a1b5
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2020 Intel Corporation, Inc
+ *
+ * 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 governinog permissions and
+ * limitations under the License.
+ */
+
+package module
+
+import (
+       "encoding/json"
+
+       "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db"
+
+       pkgerrors "github.com/pkg/errors"
+)
+
+// App contains metadata for Apps
+type App struct {
+       Metadata AppMetaData `json:"metadata"`
+}
+
+//AppMetaData contains the parameters needed for Apps
+type AppMetaData struct {
+       Name        string `json:"name"`
+       Description string `json:"description"`
+       UserData1   string `json:"userData1"`
+       UserData2   string `json:"userData2"`
+}
+
+//AppContent contains fileContent
+type AppContent struct {
+       FileContent string
+}
+
+// AppKey is the key structure that is used in the database
+type AppKey struct {
+       App                 string `json:"app"`
+       Project             string `json:"project"`
+       CompositeApp        string `json:"compositeapp"`
+       CompositeAppVersion string `json:"compositeappversion"`
+}
+
+// We will use json marshalling to convert to string to
+// preserve the underlying structure.
+func (aK AppKey) String() string {
+       out, err := json.Marshal(aK)
+       if err != nil {
+               return ""
+       }
+       return string(out)
+}
+
+// AppManager is an interface exposes the App functionality
+type AppManager interface {
+       CreateApp(a App, ac AppContent, p string, cN string, cV string) (App, error)
+       GetApp(name string, p string, cN string, cV string) (App, error)
+       GetAppContent(name string, p string, cN string, cV string) (AppContent, error)
+       DeleteApp(name string, p string, cN string, cV string) error
+}
+
+// AppClient implements the AppManager
+// It will also be used to maintain some localized state
+type AppClient struct {
+       storeName           string
+       tagMeta, tagContent string
+}
+
+// NewAppClient returns an instance of the AppClient
+// which implements the AppManager
+func NewAppClient() *AppClient {
+       return &AppClient{
+               storeName:  "orchestrator",
+               tagMeta:    "appmetadata",
+               tagContent: "appcontent",
+       }
+}
+
+// CreateApp creates a new collection based on the App
+func (v *AppClient) CreateApp(a App, ac AppContent, p string, cN string, cV string) (App, error) {
+
+       //Construct the composite key to select the entry
+       key := AppKey{
+               App:                 a.Metadata.Name,
+               Project:             p,
+               CompositeApp:        cN,
+               CompositeAppVersion: cV,
+       }
+
+       //Check if this App already exists
+       _, err := v.GetApp(a.Metadata.Name, p, cN, cV)
+       if err == nil {
+               return App{}, pkgerrors.New("App already exists")
+       }
+
+       //Check if Project exists
+       _, err = NewProjectClient().GetProject(p)
+       if err != nil {
+               return App{}, pkgerrors.New("Unable to find the project")
+       }
+
+       //check if CompositeApp with version exists
+       _, err = NewCompositeAppClient().GetCompositeApp(cN, cV, p)
+       if err != nil {
+               return App{}, pkgerrors.New("Unable to find the composite app with version")
+       }
+
+       err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, a)
+       if err != nil {
+               return App{}, pkgerrors.Wrap(err, "Creating DB Entry")
+       }
+
+       err = db.DBconn.Insert(v.storeName, key, nil, v.tagContent, ac)
+       if err != nil {
+               return App{}, pkgerrors.Wrap(err, "Creating DB Entry")
+       }
+
+       return a, nil
+}
+
+// GetApp returns the App for corresponding name
+func (v *AppClient) GetApp(name string, p string, cN string, cV string) (App, error) {
+
+       //Construct the composite key to select the entry
+       key := AppKey{
+               App:                 name,
+               Project:             p,
+               CompositeApp:        cN,
+               CompositeAppVersion: cV,
+       }
+       value, err := db.DBconn.Find(v.storeName, key, v.tagMeta)
+       if err != nil {
+               return App{}, pkgerrors.Wrap(err, "Get app")
+       }
+
+       //value is a byte array
+       if value != nil {
+               app := App{}
+               err = db.DBconn.Unmarshal(value[0], &app)
+               if err != nil {
+                       return App{}, pkgerrors.Wrap(err, "Unmarshaling Value")
+               }
+               return app, nil
+       }
+
+       return App{}, pkgerrors.New("Error getting app")
+}
+
+// GetAppContent returns content for corresponding app
+func (v *AppClient) GetAppContent(name string, p string, cN string, cV string) (AppContent, error) {
+
+       //Construct the composite key to select the entry
+       key := AppKey{
+               App:                 name,
+               Project:             p,
+               CompositeApp:        cN,
+               CompositeAppVersion: cV,
+       }
+       value, err := db.DBconn.Find(v.storeName, key, v.tagContent)
+       if err != nil {
+               return AppContent{}, pkgerrors.Wrap(err, "Get app content")
+       }
+
+       //value is a byte array
+       if value != nil {
+               ac := AppContent{}
+               err = db.DBconn.Unmarshal(value[0], &ac)
+               if err != nil {
+                       return AppContent{}, pkgerrors.Wrap(err, "Unmarshaling Value")
+               }
+               return ac, nil
+       }
+
+       return AppContent{}, pkgerrors.New("Error getting app content")
+}
+
+// DeleteApp deletes the  App from database
+func (v *AppClient) DeleteApp(name string, p string, cN string, cV string) error {
+
+       //Construct the composite key to select the entry
+       key := AppKey{
+               App:                 name,
+               Project:             p,
+               CompositeApp:        cN,
+               CompositeAppVersion: cV,
+       }
+       err := db.DBconn.Remove(v.storeName, key)
+       if err != nil {
+               return pkgerrors.Wrap(err, "Delete App Entry;")
+       }
+
+       return nil
+}
diff --git a/src/orchestrator/pkg/module/app_test.go b/src/orchestrator/pkg/module/app_test.go
new file mode 100644 (file)
index 0000000..42c08ef
--- /dev/null
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2020 Intel Corporation, Inc
+ *
+ * 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 module
+
+import (
+       "reflect"
+       "strings"
+       "testing"
+
+       "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db"
+       pkgerrors "github.com/pkg/errors"
+       //  pkgerrors "github.com/pkg/errors"
+)
+
+func TestCreateApp(t *testing.T) {
+       testCases := []struct {
+               label                  string
+               inpApp                 App
+               inpAppContent          AppContent
+               inpProject             string
+               inpCompositeAppName    string
+               inpCompositeAppVersion string
+               expectedError          string
+               mockdb                 *db.MockDB
+               expected               App
+       }{
+               {
+                       label: "Create App",
+                       inpApp: App{
+                               Metadata: AppMetaData{
+                                       Name:        "testApp",
+                                       Description: "A sample app used for unit testing",
+                                       UserData1:   "userData1",
+                                       UserData2:   "userData2",
+                               },
+                       },
+
+                       inpAppContent: AppContent{
+                               FileContent: "Sample file content",
+                       },
+                       inpProject:             "testProject",
+                       inpCompositeAppName:    "testCompositeApp",
+                       inpCompositeAppVersion: "v1",
+                       expected: App{
+                               Metadata: AppMetaData{
+                                       Name:        "testApp",
+                                       Description: "A sample app used for unit testing",
+                                       UserData1:   "userData1",
+                                       UserData2:   "userData2",
+                               },
+                       },
+                       expectedError: "",
+                       mockdb: &db.MockDB{
+                               Items: map[string]map[string][]byte{
+                                       ProjectKey{ProjectName: "testProject"}.String(): {
+                                               "projectmetadata": []byte(
+                                                       "{" +
+                                                               "\"metadata\": {" +
+                                                               "\"Name\": \"testProject\"," +
+                                                               "\"Description\": \"Test project for unit testing\"," +
+                                                               "\"UserData1\": \"userData1\"," +
+                                                               "\"UserData2\": \"userData2\"}" +
+                                                               "}"),
+                                       },
+                                       CompositeAppKey{CompositeAppName: "testCompositeApp", Version: "v1", Project: "testProject"}.String(): {
+                                               "compositeapp": []byte(
+                                                       "{" +
+                                                               "\"metadata\":{" +
+                                                               "\"Name\":\"testCompositeApp\"," +
+                                                               "\"Description\":\"Test CompositeApp for unit testing\"," +
+                                                               "\"UserData1\":\"userData1\"," +
+                                                               "\"UserData2\":\"userData2\"}," +
+                                                               "\"spec\":{" +
+                                                               "\"Version\":\"v1\"}" +
+                                                               "}"),
+                                       },
+                               },
+                       },
+               },
+       }
+
+       for _, testCase := range testCases {
+               t.Run(testCase.label, func(t *testing.T) {
+                       db.DBconn = testCase.mockdb
+                       impl := NewAppClient()
+                       got, err := impl.CreateApp(testCase.inpApp, testCase.inpAppContent, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion)
+                       if err != nil {
+                               if testCase.expectedError == "" {
+                                       t.Fatalf("Create returned an unexpected error %s", err)
+                               }
+                               if strings.Contains(err.Error(), testCase.expectedError) == false {
+                                       t.Fatalf("Create returned an unexpected error %s", err)
+                               }
+                       } else {
+                               if reflect.DeepEqual(testCase.expected, got) == false {
+                                       t.Errorf("Create returned unexpected body: got %v;"+
+                                               " expected %v", got, testCase.expected)
+                               }
+                       }
+               })
+       }
+}
+
+func TestGetApp(t *testing.T) {
+
+       testCases := []struct {
+               label                  string
+               inpApp                 string
+               inpProject             string
+               inpCompositeAppName    string
+               inpCompositeAppVersion string
+               expectedError          string
+               mockdb                 *db.MockDB
+               expected               App
+       }{
+               {
+                       label:                  "Get Composite App",
+                       inpApp:                 "testApp",
+                       inpProject:             "testProject",
+                       inpCompositeAppName:    "testCompositeApp",
+                       inpCompositeAppVersion: "v1",
+                       expected: App{
+                               Metadata: AppMetaData{
+                                       Name:        "testApp",
+                                       Description: "Test App for unit testing",
+                                       UserData1:   "userData1",
+                                       UserData2:   "userData2",
+                               },
+                       },
+                       expectedError: "",
+                       mockdb: &db.MockDB{
+                               Items: map[string]map[string][]byte{
+                                       AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): {
+                                               "appmetadata": []byte(
+                                                       "{" +
+                                                               "\"metadata\": {" +
+                                                               "\"Name\": \"testApp\"," +
+                                                               "\"Description\": \"Test App for unit testing\"," +
+                                                               "\"UserData1\": \"userData1\"," +
+                                                               "\"UserData2\": \"userData2\"}" +
+                                                               "}"),
+                                               "appcontent": []byte(
+                                                       "{" +
+                                                               "\"FileContent\": \"sample file content\"" +
+                                                               "}"),
+                                       },
+                               },
+                       },
+               },
+               {
+                       label:         "Get Error",
+                       expectedError: "DB Error",
+                       mockdb: &db.MockDB{
+                               Err: pkgerrors.New("DB Error"),
+                       },
+               },
+       }
+
+       for _, testCase := range testCases {
+               t.Run(testCase.label, func(t *testing.T) {
+                       db.DBconn = testCase.mockdb
+                       impl := NewAppClient()
+                       got, err := impl.GetApp(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion)
+                       if err != nil {
+                               if testCase.expectedError == "" {
+                                       t.Fatalf("Get returned an unexpected error: %s", err)
+                               }
+                               if strings.Contains(err.Error(), testCase.expectedError) == false {
+                                       t.Fatalf("Get returned an unexpected error: %s", err)
+                               }
+                       } else {
+                               if reflect.DeepEqual(testCase.expected, got) == false {
+                                       t.Errorf("Get returned unexpected body: got %v;"+
+                                               " expected %v", got, testCase.expected)
+                               }
+                       }
+               })
+       }
+}
+
+func TestGetAppContent(t *testing.T) {
+
+       testCases := []struct {
+               label                  string
+               inpApp                 string
+               inpProject             string
+               inpCompositeAppName    string
+               inpCompositeAppVersion string
+               expectedError          string
+               mockdb                 *db.MockDB
+               expected               AppContent
+       }{
+               {
+                       label:                  "Get App content",
+                       inpApp:                 "testApp",
+                       inpProject:             "testProject",
+                       inpCompositeAppName:    "testCompositeApp",
+                       inpCompositeAppVersion: "v1",
+                       expected: AppContent{
+                               FileContent: "Samplefilecontent",
+                       },
+                       expectedError: "",
+                       mockdb: &db.MockDB{
+                               Items: map[string]map[string][]byte{
+                                       AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): {
+                                               "appmetadata": []byte(
+                                                       "{" +
+                                                               "\"metadata\": {" +
+                                                               "\"Name\": \"testApp\"," +
+                                                               "\"Description\": \"Test App for unit testing\"," +
+                                                               "\"UserData1\": \"userData1\"," +
+                                                               "\"UserData2\": \"userData2\"}" +
+                                                               "}"),
+                                               "appcontent": []byte(
+                                                       "{" +
+                                                               "\"FileContent\": \"Samplefilecontent\"" +
+                                                               "}"),
+                                       },
+                               },
+                       },
+               },
+               {
+                       label:         "Get Error",
+                       expectedError: "DB Error",
+                       mockdb: &db.MockDB{
+                               Err: pkgerrors.New("DB Error"),
+                       },
+               },
+       }
+
+       for _, testCase := range testCases {
+               t.Run(testCase.label, func(t *testing.T) {
+                       db.DBconn = testCase.mockdb
+                       impl := NewAppClient()
+                       got, err := impl.GetAppContent(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion)
+                       if err != nil {
+                               if testCase.expectedError == "" {
+                                       t.Fatalf("Get returned an unexpected error: %s", err)
+                               }
+                               if strings.Contains(err.Error(), testCase.expectedError) == false {
+                                       t.Fatalf("Get returned an unexpected error: %s", err)
+                               }
+                       } else {
+                               if reflect.DeepEqual(testCase.expected, got) == false {
+                                       t.Errorf("Get returned unexpected body: got %v;"+
+                                               " expected %v", got, testCase.expected)
+                               }
+                       }
+               })
+       }
+}
+
+func TestDeleteApp(t *testing.T) {
+
+       testCases := []struct {
+               label                  string
+               inpApp                 string
+               inpProject             string
+               inpCompositeAppName    string
+               inpCompositeAppVersion string
+               expectedError          string
+               mockdb                 *db.MockDB
+       }{
+               {
+                       label:                  "Delete App",
+                       inpApp:                 "testApp",
+                       inpProject:             "testProject",
+                       inpCompositeAppName:    "testCompositeApp",
+                       inpCompositeAppVersion: "v1",
+                       mockdb: &db.MockDB{
+                               Items: map[string]map[string][]byte{
+                                       AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): {
+                                               "appmetadata": []byte(
+                                                       "{" +
+                                                               "\"metadata\": {" +
+                                                               "\"Name\": \"testApp\"," +
+                                                               "\"Description\": \"Test App for unit testing\"," +
+                                                               "\"UserData1\": \"userData1\"," +
+                                                               "\"UserData2\": \"userData2\"}" +
+                                                               "}"),
+                                               "appcontent": []byte(
+                                                       "{" +
+                                                               "\"FileContent\": \"Samplefilecontent\"" +
+                                                               "}"),
+                                       },
+                               },
+                       },
+               },
+               {
+                       label:         "Delete Error",
+                       expectedError: "DB Error",
+                       mockdb: &db.MockDB{
+                               Err: pkgerrors.New("DB Error"),
+                       },
+               },
+       }
+
+       for _, testCase := range testCases {
+               t.Run(testCase.label, func(t *testing.T) {
+                       db.DBconn = testCase.mockdb
+                       impl := NewAppClient()
+                       err := impl.DeleteApp(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion)
+                       if err != nil {
+                               if testCase.expectedError == "" {
+                                       t.Fatalf("Delete returned an unexpected error %s", err)
+                               }
+                               if strings.Contains(err.Error(), testCase.expectedError) == false {
+                                       t.Fatalf("Delete returned an unexpected error %s", err)
+                               }
+                       }
+               })
+       }
+}
index 74fbe0d..59fbbab 100644 (file)
@@ -105,7 +105,7 @@ func (v *CompositeAppClient) CreateCompositeApp(c CompositeApp, p string) (Compo
                return CompositeApp{}, pkgerrors.New("Unable to find the project")
        }
 
-       err = db.DBconn.Create(v.storeName, key, v.tagMeta, c)
+       err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, c)
        if err != nil {
                return CompositeApp{}, pkgerrors.Wrap(err, "Creating DB Entry")
        }
@@ -122,7 +122,7 @@ func (v *CompositeAppClient) GetCompositeApp(name string, version string, p stri
                Version:          version,
                Project:          p,
        }
-       value, err := db.DBconn.Read(v.storeName, key, v.tagMeta)
+       value, err := db.DBconn.Find(v.storeName, key, v.tagMeta)
        if err != nil {
                return CompositeApp{}, pkgerrors.Wrap(err, "Get composite application")
        }
@@ -130,7 +130,7 @@ func (v *CompositeAppClient) GetCompositeApp(name string, version string, p stri
        //value is a byte array
        if value != nil {
                compApp := CompositeApp{}
-               err = db.DBconn.Unmarshal(value, &compApp)
+               err = db.DBconn.Unmarshal(value[0], &compApp)
                if err != nil {
                        return CompositeApp{}, pkgerrors.Wrap(err, "Unmarshaling Value")
                }
@@ -149,7 +149,7 @@ func (v *CompositeAppClient) DeleteCompositeApp(name string, version string, p s
                Version:          version,
                Project:          p,
        }
-       err := db.DBconn.Delete(v.storeName, key, v.tagMeta)
+       err := db.DBconn.Remove(v.storeName, key)
        if err != nil {
                return pkgerrors.Wrap(err, "Delete CompositeApp Entry;")
        }
index 8f2948d..77a2f5b 100644 (file)
@@ -20,6 +20,7 @@ package module
 type Client struct {
        Project                *ProjectClient
        CompositeApp           *CompositeAppClient
+       App                    *AppClient
        Controller             *ControllerClient
        Cluster                *ClusterClient
        GenericPlacementIntent *GenericPlacementIntentClient
@@ -36,6 +37,7 @@ func NewClient() *Client {
        c := &Client{}
        c.Project = NewProjectClient()
        c.CompositeApp = NewCompositeAppClient()
+       c.App = NewAppClient()
        c.Controller = NewControllerClient()
        c.Cluster = NewClusterClient()
        c.GenericPlacementIntent = NewGenericPlacementIntentClient()