diff options
Diffstat (limited to 'src/orchestrator')
30 files changed, 3491 insertions, 51 deletions
diff --git a/src/orchestrator/api/add_intents_handler.go b/src/orchestrator/api/add_intents_handler.go new file mode 100644 index 00000000..dfe1a496 --- /dev/null +++ b/src/orchestrator/api/add_intents_handler.go @@ -0,0 +1,136 @@ +/* + * 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 ( + "encoding/json" + "io" + "net/http" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + "github.com/gorilla/mux" +) + +type intentHandler struct { + client moduleLib.IntentManager +} + +func (h intentHandler) addIntentHandler(w http.ResponseWriter, r *http.Request) { + var i moduleLib.Intent + + err := json.NewDecoder(r.Body).Decode(&i) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + if i.MetaData.Name == "" { + http.Error(w, "Missing Intent in POST request", http.StatusBadRequest) + return + } + + vars := mux.Vars(r) + p := vars["project-name"] + ca := vars["composite-app-name"] + v := vars["composite-app-version"] + d := vars["deployment-intent-group-name"] + + intent, addError := h.client.AddIntent(i, p, ca, v, d) + if addError != nil { + http.Error(w, addError.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(intent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (h intentHandler) getIntentHandler(w http.ResponseWriter, r *http.Request) { + + vars := mux.Vars(r) + + i := vars["intent-name"] + if i == "" { + http.Error(w, "Missing intentName in GET request", http.StatusBadRequest) + return + } + + p := vars["project-name"] + if p == "" { + http.Error(w, "Missing projectName in GET request", http.StatusBadRequest) + return + } + ca := vars["composite-app-name"] + if ca == "" { + http.Error(w, "Missing compositeAppName in GET request", http.StatusBadRequest) + return + } + + v := vars["composite-app-version"] + if v == "" { + http.Error(w, "Missing version of compositeApp in GET request", http.StatusBadRequest) + return + } + + di := vars["deployment-intent-group-name"] + if di == "" { + http.Error(w, "Missing name of DeploymentIntentGroup in GET request", http.StatusBadRequest) + return + } + + intent, err := h.client.GetIntent(i, p, ca, v, di) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(intent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (h intentHandler) deleteIntentHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + i := vars["intent-name"] + p := vars["project-name"] + ca := vars["composite-app-name"] + v := vars["composite-app-version"] + di := vars["deployment-intent-group-name"] + + err := h.client.DeleteIntent(i, p, ca, v, di) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/api.go b/src/orchestrator/api/api.go index 5e05b31b..9b33daf2 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -1,33 +1,47 @@ /* -Copyright 2020 Intel Corporation. -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. -*/ - + * 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 ( - moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" - "github.com/gorilla/mux" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" ) var moduleClient *moduleLib.Client // NewRouter creates a router that registers the various urls that are supported -func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient moduleLib.CompositeAppManager, ControllerClient moduleLib.ControllerManager, clusterClient moduleLib.ClusterManager) *mux.Router { +func NewRouter(projectClient moduleLib.ProjectManager, + compositeAppClient moduleLib.CompositeAppManager, + ControllerClient moduleLib.ControllerManager, + clusterClient moduleLib.ClusterManager, + genericPlacementIntentClient moduleLib.GenericPlacementIntentManager, + appIntentClient moduleLib.AppIntentManager, + deploymentIntentGrpClient moduleLib.DeploymentIntentGroupManager, + intentClient moduleLib.IntentManager, + compositeProfileClient moduleLib.CompositeProfileManager, + appProfileClient moduleLib.AppProfileManager) *mux.Router { router := mux.NewRouter().PathPrefix("/v2").Subrouter() + moduleClient = moduleLib.NewClient() + + //setting routes for project if projectClient == nil { projectClient = moduleClient.Project + } projHandler := projectHandler{ client: projectClient, @@ -48,17 +62,46 @@ func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient module router.HandleFunc("/projects/{project-name}", projHandler.getHandler).Methods("GET") router.HandleFunc("/projects/{project-name}", projHandler.deleteHandler).Methods("DELETE") + //setting routes for compositeApp if compositeAppClient == nil { compositeAppClient = moduleClient.CompositeApp } compAppHandler := compositeAppHandler{ client: compositeAppClient, } - router.HandleFunc("/projects/{project-name}/composite-apps", compAppHandler.createHandler).Methods("POST") 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 compositeProfileClient == nil { + compositeProfileClient = moduleClient.CompositeProfile + } + compProfilepHandler := compositeProfileHandler{ + client: compositeProfileClient, + } + if appProfileClient == nil { + appProfileClient = moduleClient.AppProfile + } + appProfileHandler := appProfileHandler{ + client: appProfileClient, + } + + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles", compProfilepHandler.createHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles", compProfilepHandler.getHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}", compProfilepHandler.getHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}", compProfilepHandler.deleteHandler).Methods("DELETE") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles", appProfileHandler.createAppProfileHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles", appProfileHandler.getAppProfileHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles", appProfileHandler.getAppProfileHandler).Queries("app-name", "{app-name}") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles/{app-profile}", appProfileHandler.getAppProfileHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles/{app-profile}", appProfileHandler.deleteAppProfileHandler).Methods("DELETE") + + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles", appProfileHandler.createAppProfileHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles", appProfileHandler.getAppProfileHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles", appProfileHandler.getAppProfileHandler).Queries("app-name", "{app-name}") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles/{app-profile}", appProfileHandler.getAppProfileHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/composite-profiles/{composite-profile-name}/profiles/{app-profile}", appProfileHandler.deleteAppProfileHandler).Methods("DELETE") + router.HandleFunc("/controllers", controlHandler.createHandler).Methods("POST") router.HandleFunc("/controllers", controlHandler.createHandler).Methods("PUT") router.HandleFunc("/controllers/{controller-name}", controlHandler.getHandler).Methods("GET") @@ -79,6 +122,55 @@ func NewRouter(projectClient moduleLib.ProjectManager, compositeAppClient module router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/kv-pairs", clusterHandler.getClusterKvPairsHandler).Methods("GET") router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/kv-pairs/{kvpair}", clusterHandler.getClusterKvPairsHandler).Methods("GET") router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/kv-pairs/{kvpair}", clusterHandler.deleteClusterKvPairsHandler).Methods("DELETE") + //setting routes for genericPlacementIntent + if genericPlacementIntentClient == nil { + genericPlacementIntentClient = moduleClient.GenericPlacementIntent + } + + genericPlacementIntentHandler := genericPlacementIntentHandler{ + client: genericPlacementIntentClient, + } + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/generic-placement-intents", genericPlacementIntentHandler.createGenericPlacementIntentHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/generic-placement-intents/{intent-name}", genericPlacementIntentHandler.getGenericPlacementHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/generic-placement-intents/{intent-name}", genericPlacementIntentHandler.deleteGenericPlacementHandler).Methods("DELETE") + + //setting routes for AppIntent + if appIntentClient == nil { + appIntentClient = moduleClient.AppIntent + } + + appIntentHandler := appIntentHandler{ + client: appIntentClient, + } + + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/generic-placement-intents/{intent-name}/app-intents", appIntentHandler.createAppIntentHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/generic-placement-intents/{intent-name}/app-intents/{app-intent-name}", appIntentHandler.getAppIntentHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/generic-placement-intents/{intent-name}/app-intents/{app-intent-name}", appIntentHandler.deleteAppIntentHandler).Methods("DELETE") + + //setting routes for deploymentIntentGroup + if deploymentIntentGrpClient == nil { + deploymentIntentGrpClient = moduleClient.DeploymentIntentGroup + } + + deploymentIntentGrpHandler := deploymentIntentGroupHandler{ + client: deploymentIntentGrpClient, + } + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/deployment-intent-groups", deploymentIntentGrpHandler.createDeploymentIntentGroupHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/deployment-intent-groups/{deployment-intent-group-name}", deploymentIntentGrpHandler.getDeploymentIntentGroupHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/deployment-intent-groups/{deployment-intent-group-name}", deploymentIntentGrpHandler.deleteDeploymentIntentGroupHandler).Methods("DELETE") + + // setting routes for AddingIntents + if intentClient == nil { + intentClient = moduleClient.Intent + } + + intentHandler := intentHandler{ + client: intentClient, + } + + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/deployment-intent-groups/{deployment-intent-group-name}/intents", intentHandler.addIntentHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/deployment-intent-groups/{deployment-intent-group-name}/intents/{intent-name}", intentHandler.getIntentHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{composite-app-version}/deployment-intent-groups/{deployment-intent-group-name}/intents/{intent-name}", intentHandler.deleteIntentHandler).Methods("DELETE") return router } diff --git a/src/orchestrator/api/app_intent_handler.go b/src/orchestrator/api/app_intent_handler.go new file mode 100644 index 00000000..ab510c8e --- /dev/null +++ b/src/orchestrator/api/app_intent_handler.go @@ -0,0 +1,138 @@ +/* + * 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 ( + "encoding/json" + "github.com/gorilla/mux" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + "io" + "net/http" +) + +/* Used to store backend implementation objects +Also simplifies mocking for unit testing purposes +*/ +type appIntentHandler struct { + client moduleLib.AppIntentManager +} + +// createAppIntentHandler handles the create operation of intent +func (h appIntentHandler) createAppIntentHandler(w http.ResponseWriter, r *http.Request) { + + var a moduleLib.AppIntent + + err := json.NewDecoder(r.Body).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 + } + + if a.MetaData.Name == "" { + http.Error(w, "Missing AppIntentName in POST request", http.StatusBadRequest) + return + } + + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + version := vars["composite-app-version"] + intent := vars["intent-name"] + + appIntent, createErr := h.client.CreateAppIntent(a, projectName, compositeAppName, version, intent) + if createErr != nil { + http.Error(w, createErr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(appIntent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (h appIntentHandler) getAppIntentHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + p := vars["project-name"] + if p == "" { + http.Error(w, "Missing projectName in GET request", http.StatusBadRequest) + return + } + ca := vars["composite-app-name"] + if ca == "" { + http.Error(w, "Missing compositeAppName in GET request", http.StatusBadRequest) + return + } + + v := vars["composite-app-version"] + if v == "" { + http.Error(w, "Missing version of compositeApp in GET request", http.StatusBadRequest) + return + } + + i := vars["intent-name"] + if i == "" { + http.Error(w, "Missing genericPlacementIntentName in GET request", http.StatusBadRequest) + return + } + + ai := vars["app-intent-name"] + if ai == "" { + http.Error(w, "Missing appIntentName in GET request", http.StatusBadRequest) + return + } + + appIntent, err := h.client.GetAppIntent(ai, p, ca, v, i) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(appIntent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + +} + +func (h appIntentHandler) deleteAppIntentHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + p := vars["project-name"] + ca := vars["composite-app-name"] + v := vars["composite-app-version"] + i := vars["intent-name"] + ai := vars["app-intent-name"] + + err := h.client.DeleteAppIntent(ai, p, ca, v, i) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/app_profilehandler.go b/src/orchestrator/api/app_profilehandler.go new file mode 100644 index 00000000..16423483 --- /dev/null +++ b/src/orchestrator/api/app_profilehandler.go @@ -0,0 +1,266 @@ +/* + * 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" + pkgerrors "github.com/pkg/errors" +) + +/* Used to store backend implementation objects +Also simplifies mocking for unit testing purposes +*/ +type appProfileHandler struct { + client moduleLib.AppProfileManager +} + +// createAppProfileHandler handles the create operation +func (h appProfileHandler) createAppProfileHandler(w http.ResponseWriter, r *http.Request) { + + vars := mux.Vars(r) + project := vars["project-name"] + compositeApp := vars["composite-app-name"] + compositeAppVersion := vars["composite-app-version"] + compositeProfile := vars["composite-profile-name"] + + var ap moduleLib.AppProfile + var ac moduleLib.AppProfileContent + + // Implemenation using multipart form + // Review and enable/remove at a later date + // 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(&ap) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + 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.Profile = base64.StdEncoding.EncodeToString(content) + + // Name is required. + if ap.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, ap, ac) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// getHandler handles the GET operations on AppProfile +func (h appProfileHandler) getAppProfileHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + project := vars["project-name"] + compositeApp := vars["composite-app-name"] + compositeAppVersion := vars["composite-app-version"] + compositeProfile := vars["composite-profile-name"] + name := vars["app-profile"] + appName := r.URL.Query().Get("app-name") + + if len(name) != 0 && len(appName) != 0 { + http.Error(w, pkgerrors.New("Invalid query").Error(), http.StatusInternalServerError) + return + } + + // handle the get all app profiles case - return a list of only the json parts + if len(name) == 0 && len(appName) == 0 { + var retList []moduleLib.AppProfile + + ret, err := h.client.GetAppProfiles(project, compositeApp, compositeAppVersion, compositeProfile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, ap := range ret { + retList = append(retList, ap) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(retList) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + + accepted, _, err := mime.ParseMediaType(r.Header.Get("Accept")) + if err != nil { + http.Error(w, err.Error(), http.StatusNotAcceptable) + return + } + + var retAppProfile moduleLib.AppProfile + var retAppProfileContent moduleLib.AppProfileContent + + if len(appName) != 0 { + retAppProfile, err = h.client.GetAppProfileByApp(project, compositeApp, compositeAppVersion, compositeProfile, appName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + retAppProfileContent, err = h.client.GetAppProfileContentByApp(project, compositeApp, compositeAppVersion, compositeProfile, appName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + retAppProfile, err = h.client.GetAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + retAppProfileContent, err = h.client.GetAppProfileContent(project, compositeApp, compositeAppVersion, compositeProfile, name) + 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(retAppProfile); 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"}}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + kc_bytes, err := base64.StdEncoding.DecodeString(retAppProfileContent.Profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = pw.Write(kc_bytes) + 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(retAppProfile) + 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) + kc_bytes, err := base64.StdEncoding.DecodeString(retAppProfileContent.Profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(kc_bytes) + 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 + } +} + +// deleteHandler handles the delete operations on AppProfile +func (h appProfileHandler) deleteAppProfileHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + project := vars["project-name"] + compositeApp := vars["composite-app-name"] + compositeAppVersion := vars["composite-app-version"] + compositeProfile := vars["composite-profile-name"] + name := vars["app-profile"] + + err := h.client.DeleteAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/clusterhandler.go b/src/orchestrator/api/clusterhandler.go index 7e769f31..ac4191e1 100644 --- a/src/orchestrator/api/clusterhandler.go +++ b/src/orchestrator/api/clusterhandler.go @@ -255,12 +255,12 @@ func (h clusterHandler) getClusterHandler(w http.ResponseWriter, r *http.Request http.Error(w, err.Error(), http.StatusInternalServerError) return } - kc_bytes, err := base64.StdEncoding.DecodeString(retKubeconfig.Kubeconfig) + kcBytes, err := base64.StdEncoding.DecodeString(retKubeconfig.Kubeconfig) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - _, err = pw.Write(kc_bytes) + _, err = pw.Write(kcBytes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -276,12 +276,12 @@ func (h clusterHandler) getClusterHandler(w http.ResponseWriter, r *http.Request case "application/octet-stream": w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(http.StatusOK) - kc_bytes, err := base64.StdEncoding.DecodeString(retKubeconfig.Kubeconfig) + kcBytes, err := base64.StdEncoding.DecodeString(retKubeconfig.Kubeconfig) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - _, err = w.Write(kc_bytes) + _, err = w.Write(kcBytes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/src/orchestrator/api/clusterhandler_test.go b/src/orchestrator/api/clusterhandler_test.go index db1b6f9f..71afdd1b 100644 --- a/src/orchestrator/api/clusterhandler_test.go +++ b/src/orchestrator/api/clusterhandler_test.go @@ -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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.clusterClient, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/api/composite_profilehandler.go b/src/orchestrator/api/composite_profilehandler.go new file mode 100644 index 00000000..66c64dda --- /dev/null +++ b/src/orchestrator/api/composite_profilehandler.go @@ -0,0 +1,151 @@ +/* + * 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 ( + "encoding/json" + "io" + "net/http" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + "github.com/gorilla/mux" +) + +/* Used to store backend implementation objects +Also simplifies mocking for unit testing purposes +*/ +type compositeProfileHandler struct { + client moduleLib.CompositeProfileManager +} + +// createCompositeProfileHandler handles the create operation of intent +func (h compositeProfileHandler) createHandler(w http.ResponseWriter, r *http.Request) { + + var cpf moduleLib.CompositeProfile + + err := json.NewDecoder(r.Body).Decode(&cpf) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + if cpf.Metadata.Name == "" { + http.Error(w, "Missing compositeProfileName in POST request", http.StatusBadRequest) + return + } + + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + version := vars["composite-app-version"] + + cProf, createErr := h.client.CreateCompositeProfile(cpf, projectName, compositeAppName, version) + if createErr != nil { + http.Error(w, createErr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(cProf) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// getHandler handles the GET operations on CompositeProfile +func (h compositeProfileHandler) getHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + cProfName := vars["composite-profile-name"] + + projectName := vars["project-name"] + if projectName == "" { + http.Error(w, "Missing projectName in GET request", http.StatusBadRequest) + return + } + compositeAppName := vars["composite-app-name"] + if compositeAppName == "" { + http.Error(w, "Missing compositeAppName in GET request", http.StatusBadRequest) + return + } + + version := vars["composite-app-version"] + if version == "" { + http.Error(w, "Missing version in GET request", http.StatusBadRequest) + return + } + + // handle the get all composite profile case + if len(cProfName) == 0 { + var retList []moduleLib.CompositeProfile + + ret, err := h.client.GetCompositeProfiles(projectName, compositeAppName, version) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, cl := range ret { + retList = append(retList, moduleLib.CompositeProfile{Metadata: cl.Metadata}) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(retList) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + + cProf, err := h.client.GetCompositeProfile(cProfName, projectName, compositeAppName, version) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(cProf) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// deleteHandler handles the delete operations on CompostiteProfile +func (h compositeProfileHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + c := vars["composite-profile-name"] + p := vars["project-name"] + ca := vars["composite-app-name"] + v := vars["composite-app-version"] + + err := h.client.DeleteCompositeProfile(c, p, ca, v) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/composite_profilehandler_test.go b/src/orchestrator/api/composite_profilehandler_test.go new file mode 100644 index 00000000..360653c7 --- /dev/null +++ b/src/orchestrator/api/composite_profilehandler_test.go @@ -0,0 +1,151 @@ +/* + * 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/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" +) + +//Creating an embedded interface via anonymous variable +//This allows us to make mockDB satisfy the DatabaseConnection +//interface even if we are not implementing all the methods in it +type mockCompositeProfileManager struct { + // Items and err will be used to customize each test + // via a localized instantiation of mockCompositeProfileManager + Items []moduleLib.CompositeProfile + Err error +} + +func (m *mockCompositeProfileManager) CreateCompositeProfile(inp moduleLib.CompositeProfile, p string, ca string, + v string) (moduleLib.CompositeProfile, error) { + if m.Err != nil { + return moduleLib.CompositeProfile{}, m.Err + } + + return m.Items[0], nil +} + +func (m *mockCompositeProfileManager) GetCompositeProfile(name string, projectName string, + compositeAppName string, version string) (moduleLib.CompositeProfile, error) { + if m.Err != nil { + return moduleLib.CompositeProfile{}, m.Err + } + + return m.Items[0], nil +} + +func (m *mockCompositeProfileManager) GetCompositeProfiles(projectName string, + compositeAppName string, version string) ([]moduleLib.CompositeProfile, error) { + if m.Err != nil { + return []moduleLib.CompositeProfile{}, m.Err + } + + return m.Items, nil +} + +func (m *mockCompositeProfileManager) DeleteCompositeProfile(name string, projectName string, + compositeAppName string, version string) error { + return m.Err +} + +func Test_compositeProfileHandler_createHandler(t *testing.T) { + testCases := []struct { + label string + reader io.Reader + expected moduleLib.CompositeProfile + expectedCode int + cProfClient *mockCompositeProfileManager + }{ + { + label: "Missing Body Failure", + expectedCode: http.StatusBadRequest, + cProfClient: &mockCompositeProfileManager{}, + }, + { + label: "Create Composite Profile", + expectedCode: http.StatusCreated, + reader: bytes.NewBuffer([]byte(`{ + "metadata" : { + "name": "testCompositeProfile", + "description": "Test CompositeProfile used for unit testing", + "userData1": "data1", + "userData2": "data2" + } + }`)), + expected: moduleLib.CompositeProfile{ + Metadata: moduleLib.CompositeProfileMetadata{ + Name: "testCompositeProfile", + Description: "Test CompositeProfile used for unit testing", + UserData1: "data1", + UserData2: "data2", + }, + }, + cProfClient: &mockCompositeProfileManager{ + //Items that will be returned by the mocked Client + Items: []moduleLib.CompositeProfile{ + moduleLib.CompositeProfile{ + Metadata: moduleLib.CompositeProfileMetadata{ + Name: "testCompositeProfile", + Description: "Test CompositeProfile used for unit testing", + UserData1: "data1", + UserData2: "data2", + }, + }, + }, + }, + }, + { + label: "Missing Composite Profile Name in Request Body", + reader: bytes.NewBuffer([]byte(`{ + "description":"test description" + }`)), + expectedCode: http.StatusBadRequest, + cProfClient: &mockCompositeProfileManager{}, + }, + } + 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)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusCreated + if resp.StatusCode == http.StatusCreated { + got := moduleLib.CompositeProfile{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("createHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } + +} diff --git a/src/orchestrator/api/controllerhandler_test.go b/src/orchestrator/api/controllerhandler_test.go index c91943ad..ab0aeed8 100644 --- a/src/orchestrator/api/controllerhandler_test.go +++ b/src/orchestrator/api/controllerhandler_test.go @@ -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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(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)) + resp := executeRequest(request, NewRouter(nil, nil, testCase.controllerClient, nil, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/api/deployment_intent_groups_handler.go b/src/orchestrator/api/deployment_intent_groups_handler.go new file mode 100644 index 00000000..3f5b3969 --- /dev/null +++ b/src/orchestrator/api/deployment_intent_groups_handler.go @@ -0,0 +1,133 @@ +/* + * 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 ( + "encoding/json" + "io" + "net/http" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + "github.com/gorilla/mux" +) + +/* Used to store backend implementation objects +Also simplifies mocking for unit testing purposes +*/ +type deploymentIntentGroupHandler struct { + client moduleLib.DeploymentIntentGroupManager +} + +// createDeploymentIntentGroupHandler handles the create operation of DeploymentIntentGroup +func (h deploymentIntentGroupHandler) createDeploymentIntentGroupHandler(w http.ResponseWriter, r *http.Request) { + + var d moduleLib.DeploymentIntentGroup + + err := json.NewDecoder(r.Body).Decode(&d) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + if d.MetaData.Name == "" { + http.Error(w, "Missing deploymentIntentGroupName in POST request", http.StatusBadRequest) + return + } + + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + version := vars["composite-app-version"] + + dIntent, createErr := h.client.CreateDeploymentIntentGroup(d, projectName, compositeAppName, version) + if createErr != nil { + http.Error(w, createErr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(dIntent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (h deploymentIntentGroupHandler) getDeploymentIntentGroupHandler(w http.ResponseWriter, r *http.Request) { + + vars := mux.Vars(r) + + p := vars["project-name"] + if p == "" { + http.Error(w, "Missing projectName in GET request", http.StatusBadRequest) + return + } + ca := vars["composite-app-name"] + if ca == "" { + http.Error(w, "Missing compositeAppName in GET request", http.StatusBadRequest) + return + } + + v := vars["composite-app-version"] + if v == "" { + http.Error(w, "Missing version of compositeApp in GET request", http.StatusBadRequest) + return + } + + di := vars["deployment-intent-group-name"] + if v == "" { + http.Error(w, "Missing name of DeploymentIntentGroup in GET request", http.StatusBadRequest) + return + } + + dIntentGrp, err := h.client.GetDeploymentIntentGroup(di, p, ca, v) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(dIntentGrp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + +} + +func (h deploymentIntentGroupHandler) deleteDeploymentIntentGroupHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + p := vars["project-name"] + ca := vars["composite-app-name"] + v := vars["composite-app-version"] + di := vars["deployment-intent-group-name"] + + err := h.client.DeleteDeploymentIntentGroup(di, p, ca, v) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/generic_placement_intent_handler.go b/src/orchestrator/api/generic_placement_intent_handler.go new file mode 100644 index 00000000..e186735c --- /dev/null +++ b/src/orchestrator/api/generic_placement_intent_handler.go @@ -0,0 +1,130 @@ +/* + * 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 ( + "encoding/json" + "io" + "net/http" + + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + "github.com/gorilla/mux" +) + +/* Used to store backend implementation objects +Also simplifies mocking for unit testing purposes +*/ +type genericPlacementIntentHandler struct { + client moduleLib.GenericPlacementIntentManager +} + +// createGenericPlacementIntentHandler handles the create operation of intent +func (h genericPlacementIntentHandler) createGenericPlacementIntentHandler(w http.ResponseWriter, r *http.Request) { + + var g moduleLib.GenericPlacementIntent + + err := json.NewDecoder(r.Body).Decode(&g) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + if g.MetaData.Name == "" { + http.Error(w, "Missing genericPlacementIntentName in POST request", http.StatusBadRequest) + return + } + + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + version := vars["composite-app-version"] + + gPIntent, createErr := h.client.CreateGenericPlacementIntent(g, projectName, compositeAppName, version) + if createErr != nil { + http.Error(w, createErr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(gPIntent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// getGenericPlacementHandler handles the GET operations on intent +func (h genericPlacementIntentHandler) getGenericPlacementHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + intentName := vars["intent-name"] + if intentName == "" { + http.Error(w, "Missing genericPlacementIntentName in GET request", http.StatusBadRequest) + return + } + projectName := vars["project-name"] + if projectName == "" { + http.Error(w, "Missing projectName in GET request", http.StatusBadRequest) + return + } + compositeAppName := vars["composite-app-name"] + if compositeAppName == "" { + http.Error(w, "Missing compositeAppName in GET request", http.StatusBadRequest) + return + } + + version := vars["composite-app-version"] + if version == "" { + http.Error(w, "Missing version in GET request", http.StatusBadRequest) + return + } + + gPIntent, err := h.client.GetGenericPlacementIntent(intentName, projectName, compositeAppName, version) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(gPIntent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// deleteGenericPlacementHandler handles the delete operations on intent +func (h genericPlacementIntentHandler) deleteGenericPlacementHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + i := vars["intent-name"] + p := vars["project-name"] + ca := vars["composite-app-name"] + v := vars["composite-app-version"] + + err := h.client.DeleteGenericPlacementIntent(i, p, ca, v) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/projecthandler_test.go b/src/orchestrator/api/projecthandler_test.go index c6da4e05..af40f961 100644 --- a/src/orchestrator/api/projecthandler_test.go +++ b/src/orchestrator/api/projecthandler_test.go @@ -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)) + resp := executeRequest(request, NewRouter(testCase.projectClient, 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)) + resp := executeRequest(request, NewRouter(testCase.projectClient, 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)) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil)) //Check returned code if resp.StatusCode != testCase.expectedCode { diff --git a/src/orchestrator/cmd/main.go b/src/orchestrator/cmd/main.go index a4c46fe7..f95c057e 100644 --- a/src/orchestrator/cmd/main.go +++ b/src/orchestrator/cmd/main.go @@ -47,7 +47,7 @@ func main() { log.Fatalln("Exiting...") } - httpRouter := api.NewRouter(nil, nil, nil, nil) + httpRouter := api.NewRouter(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) log.Println("Starting Kubernetes Multicloud API") diff --git a/src/orchestrator/go.mod b/src/orchestrator/go.mod index d6fada43..547fa8ed 100644 --- a/src/orchestrator/go.mod +++ b/src/orchestrator/go.mod @@ -1,6 +1,7 @@ module github.com/onap/multicloud-k8s/src/orchestrator require ( + github.com/coreos/etcd v3.3.12+incompatible github.com/docker/engine v0.0.0-20190620014054-c513a4c6c298 github.com/ghodss/yaml v1.0.0 github.com/gogo/protobuf v1.3.1 // indirect @@ -32,3 +33,5 @@ replace ( k8s.io/client-go => k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible k8s.io/cloud-provider => k8s.io/cloud-provider v0.0.0-20190409023720-1bc0c81fa51d ) + +go 1.13 diff --git a/src/orchestrator/go.sum b/src/orchestrator/go.sum index d2015406..aeab3b50 100644 --- a/src/orchestrator/go.sum +++ b/src/orchestrator/go.sum @@ -172,6 +172,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onap/multicloud-k8s v0.0.0-20191115005109-f168ebb73d8d h1:3uFucXVv6gqa3H1u85CjoLOvGraREfD8/NL7m/9W9tc= github.com/onap/multicloud-k8s v0.0.0-20200131010833-90e13d101cf0 h1:2qDo6s4pdg/g7Vj6QGrCK02EP4jjwVehgEObnAfipSM= +github.com/onap/multicloud-k8s v0.0.0-20200229013830-7b566f287523 h1:hVu6djUEav5nKQvVZZa3FT71ZD9QbCcTI3dM+1chvFU= github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20191115005109-f168ebb73d8d h1:ucIEjqzNVeFPnQofeuBfUqro0OnilX//fajEFxuLsgA= github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20191115005109-f168ebb73d8d/go.mod h1:EnQd/vQGZR1/55IihaHxiux4ZUig/zfXZux7bfmU0S8= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/src/orchestrator/pkg/infra/db/mock.go b/src/orchestrator/pkg/infra/db/mock.go index 6b46a524..79366d10 100644 --- a/src/orchestrator/pkg/infra/db/mock.go +++ b/src/orchestrator/pkg/infra/db/mock.go @@ -45,6 +45,10 @@ func (m *MockDB) Create(table string, key Key, tag string, data interface{}) err return m.Err } +func (m *MockDB) Insert(table string, key Key, query interface{}, tag string, data interface{}) error { + return m.Err +} + func (m *MockDB) Update(table string, key Key, tag string, data interface{}) error { return m.Err } @@ -73,7 +77,22 @@ func (m *MockDB) Read(table string, key Key, tag string) ([]byte, error) { return nil, m.Err } +func (m *MockDB) Find(table string, key Key, tag string) ([][]byte, error) { + if m.Err != nil { + return nil, m.Err + } + + str := fmt.Sprintf("%v", key) + for k, v := range m.Items { + if k == str { + + return [][]byte{v[tag]}, nil + } + } + + return nil, m.Err +} + func (m *MockDB) Delete(table string, key Key, tag string) error { return m.Err } - diff --git a/src/orchestrator/pkg/infra/db/mongo_test.go b/src/orchestrator/pkg/infra/db/mongo_test.go index d506dbda..9f813ca4 100644 --- a/src/orchestrator/pkg/infra/db/mongo_test.go +++ b/src/orchestrator/pkg/infra/db/mongo_test.go @@ -76,7 +76,7 @@ func (c *mockCollection) DeleteMany(ctx context.Context, filter interface{}, func (c *mockCollection) UpdateOne(ctx context.Context, filter interface{}, update interface{}, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) { - return nil, c.Err + return nil, c.Err } func TestCreate(t *testing.T) { @@ -463,4 +463,3 @@ func TestDelete(t *testing.T) { }) } } - diff --git a/src/orchestrator/pkg/module/add_intents.go b/src/orchestrator/pkg/module/add_intents.go new file mode 100644 index 00000000..a657cce7 --- /dev/null +++ b/src/orchestrator/pkg/module/add_intents.go @@ -0,0 +1,184 @@ +/* + * 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 Addlicable 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 ( + "encoding/json" + "reflect" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// Intent shall have 2 fields - MetaData and Spec +type Intent struct { + MetaData IntentMetaData `json:"metadata"` + Spec IntentSpecData `json:"spec"` +} + +// IntentMetaData has Name, Description, userdata1, userdata2 +type IntentMetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +// IntentSpecData has Intent +type IntentSpecData struct { + Intent IntentObj `json:"intent"` +} + +// IntentObj has name of the generic placement intent +type IntentObj struct { + Generic string `json:"generic"` +} + +// ListOfIntents is a list of intents +type ListOfIntents struct { + ListOfIntents []map[string]string `json:"intent"` +} + +// IntentManager is an interface which exposes the IntentManager functionality +type IntentManager interface { + AddIntent(a Intent, p string, ca string, v string, di string) (Intent, error) + GetIntent(i string, p string, ca string, v string, di string) (Intent, error) + DeleteIntent(i string, p string, ca string, v string, di string) error +} + +// IntentKey consists of Name if the intent, Project name, CompositeApp name, +// CompositeApp version +type IntentKey struct { + Name string `json:"name"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"version"` + DeploymentIntentGroup string `json:"deployment-intent-group-name"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (ik IntentKey) String() string { + out, err := json.Marshal(ik) + if err != nil { + return "" + } + return string(out) +} + +// IntentClient implements the AddIntentManager interface +type IntentClient struct { + storeName string + tagMetaData string +} + +// NewIntentClient returns an instance of AddIntentClient +func NewIntentClient() *IntentClient { + return &IntentClient{ + storeName: "orchestrator", + tagMetaData: "addintent", + } +} + +// AddIntent adds a given intent to the deployment-intent-group and stores in the db. Other input parameters for it - projectName, compositeAppName, version, DeploymentIntentgroupName +func (c *IntentClient) AddIntent(a Intent, p string, ca string, v string, di string) (Intent, error) { + + //Check for the AddIntent already exists here. + res, err := c.GetIntent(a.MetaData.Name, p, ca, v, di) + if !reflect.DeepEqual(res, Intent{}) { + return Intent{}, pkgerrors.New("AppIntent already exists") + } + + //Check if project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return Intent{}, pkgerrors.New("Unable to find the project") + } + + //check if compositeApp exists + _, err = NewCompositeAppClient().GetCompositeApp(ca, v, p) + if err != nil { + return Intent{}, pkgerrors.New("Unable to find the composite-app") + } + + //check if DeploymentIntentGroup exists + _, err = NewDeploymentIntentGroupClient().GetDeploymentIntentGroup(di, p, ca, v) + if err != nil { + return Intent{}, pkgerrors.New("Unable to find the intent") + } + + akey := IntentKey{ + Name: a.MetaData.Name, + Project: p, + CompositeApp: ca, + Version: v, + DeploymentIntentGroup: di, + } + + err = db.DBconn.Insert(c.storeName, akey, nil, c.tagMetaData, a) + if err != nil { + return Intent{}, pkgerrors.Wrap(err, "Create DB entry error") + } + return a, nil +} + +// GetIntent returns an Intent +func (c *IntentClient) GetIntent(i string, p string, ca string, v string, di string) (Intent, error) { + + k := IntentKey{ + Name: i, + Project: p, + CompositeApp: ca, + Version: v, + DeploymentIntentGroup: di, + } + + result, err := db.DBconn.Find(c.storeName, k, c.tagMetaData) + if err != nil { + return Intent{}, pkgerrors.Wrap(err, "Get AppIntent error") + } + + if result != nil { + a := Intent{} + err = db.DBconn.Unmarshal(result[0], &a) + if err != nil { + return Intent{}, pkgerrors.Wrap(err, "Unmarshalling AppIntent") + } + return a, nil + + } + return Intent{}, pkgerrors.New("Error getting AppIntent") +} + +// DeleteIntent deletes a given intent tied to project, composite app and deployment intent group +func (c IntentClient) DeleteIntent(i string, p string, ca string, v string, di string) error { + k := IntentKey{ + Name: i, + Project: p, + CompositeApp: ca, + Version: v, + DeploymentIntentGroup: di, + } + + err := db.DBconn.Remove(c.storeName, k) + if err != nil { + return pkgerrors.Wrap(err, "Delete Project entry;") + } + return nil + +} diff --git a/src/orchestrator/pkg/module/app_intent.go b/src/orchestrator/pkg/module/app_intent.go new file mode 100644 index 00000000..70c80dac --- /dev/null +++ b/src/orchestrator/pkg/module/app_intent.go @@ -0,0 +1,195 @@ +/* + * 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 ( + "encoding/json" + "reflect" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// AppIntent has two components - metadata, spec +type AppIntent struct { + MetaData MetaData `json:"metadata"` + Spec SpecData `json:"spec"` +} + +// MetaData has - name, description, userdata1, userdata2 +type MetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +// AllOf consists of AnyOfArray and ClusterNames array +type AllOf struct { + ClusterName string `json:"cluster-name,omitempty"` + ClusterLabelName string `json:"cluster-label-name,omitempty"` + AnyOfArray []AnyOf `json:"anyOf,omitempty"` +} + +// AnyOf consists of Array of ClusterLabelNames +type AnyOf struct { + ClusterName string `json:"cluster-name,omitempty"` + ClusterLabelName string `json:"cluster-label-name,omitempty"` +} + +// IntentStruc consists of AllOfArray and AnyOfArray +type IntentStruc struct { + AllOfArray []AllOf `json:"allOf,omitempty"` + AnyOfArray []AnyOf `json:"anyOf,omitempty"` +} + +// SpecData consists of appName and intent +type SpecData struct { + AppName string `json:"app-name"` + Intent IntentStruc `json:"intent"` +} + +// AppIntentManager is an interface which exposes the +// AppIntentManager functionalities +type AppIntentManager interface { + CreateAppIntent(a AppIntent, p string, ca string, v string, i string) (AppIntent, error) + GetAppIntent(ai string, p string, ca string, v string, i string) (AppIntent, error) + DeleteAppIntent(ai string, p string, ca string, v string, i string) error +} + +// AppIntentKey is used as primary key +type AppIntentKey struct { + Name string `json:"name"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"version"` + Intent string `json:"intent-name"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (ak AppIntentKey) String() string { + out, err := json.Marshal(ak) + if err != nil { + return "" + } + return string(out) +} + +// AppIntentClient implements the AppIntentManager interface +type AppIntentClient struct { + storeName string + tagMetaData string +} + +// NewAppIntentClient returns an instance of AppIntentClient +func NewAppIntentClient() *AppIntentClient { + return &AppIntentClient{ + storeName: "orchestrator", + tagMetaData: "appintent", + } +} + +// CreateAppIntent creates an entry for AppIntent in the db. Other input parameters for it - projectName, compositeAppName, version, intentName. +func (c *AppIntentClient) CreateAppIntent(a AppIntent, p string, ca string, v string, i string) (AppIntent, error) { + + //Check for the AppIntent already exists here. + res, err := c.GetAppIntent(a.MetaData.Name, p, ca, v, i) + if !reflect.DeepEqual(res, AppIntent{}) { + return AppIntent{}, pkgerrors.New("AppIntent already exists") + } + + //Check if project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return AppIntent{}, pkgerrors.New("Unable to find the project") + } + + // check if compositeApp exists + _, err = NewCompositeAppClient().GetCompositeApp(ca, v, p) + if err != nil { + return AppIntent{}, pkgerrors.New("Unable to find the composite-app") + } + + // check if Intent exists + _, err = NewGenericPlacementIntentClient().GetGenericPlacementIntent(i, p, ca, v) + if err != nil { + return AppIntent{}, pkgerrors.New("Unable to find the intent") + } + + akey := AppIntentKey{ + Name: a.MetaData.Name, + Project: p, + CompositeApp: ca, + Version: v, + Intent: i, + } + + err = db.DBconn.Insert(c.storeName, akey, nil, c.tagMetaData, a) + if err != nil { + return AppIntent{}, pkgerrors.Wrap(err, "Create DB entry error") + } + + return a, nil +} + +// GetAppIntent shall take arguments - name of the app intent, name of the project, name of the composite app, version of the composite app and intent name. It shall return the AppIntent +func (c *AppIntentClient) GetAppIntent(ai string, p string, ca string, v string, i string) (AppIntent, error) { + + k := AppIntentKey{ + Name: ai, + Project: p, + CompositeApp: ca, + Version: v, + Intent: i, + } + + result, err := db.DBconn.Find(c.storeName, k, c.tagMetaData) + if err != nil { + return AppIntent{}, pkgerrors.Wrap(err, "Get AppIntent error") + } + + if result != nil { + a := AppIntent{} + err = db.DBconn.Unmarshal(result[0], &a) + if err != nil { + return AppIntent{}, pkgerrors.Wrap(err, "Unmarshalling AppIntent") + } + return a, nil + + } + return AppIntent{}, pkgerrors.New("Error getting AppIntent") +} + +// DeleteAppIntent delete an AppIntent +func (c *AppIntentClient) DeleteAppIntent(ai string, p string, ca string, v string, i string) error { + k := AppIntentKey{ + Name: ai, + Project: p, + CompositeApp: ca, + Version: v, + Intent: i, + } + + err := db.DBconn.Remove(c.storeName, k) + if err != nil { + return pkgerrors.Wrap(err, "Delete Project entry;") + } + return nil + +} diff --git a/src/orchestrator/pkg/module/app_intent_test.go b/src/orchestrator/pkg/module/app_intent_test.go new file mode 100644 index 00000000..5a4f7693 --- /dev/null +++ b/src/orchestrator/pkg/module/app_intent_test.go @@ -0,0 +1,271 @@ +/* + * 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" +) + +func TestCreateAppIntent(t *testing.T) { + testCases := []struct { + label string + inputAppIntent AppIntent + inputProject string + inputCompositeApp string + inputCompositeAppVersion string + inputGenericPlacementIntent string + expectedError string + mockdb *db.MockDB + expected AppIntent + }{ + { + label: "Create AppIntent", + inputAppIntent: AppIntent{ + MetaData: MetaData{ + Name: "testAppIntent", + Description: "A sample AppIntent", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: SpecData{ + AppName: "SampleApp", + Intent: IntentStruc{ + AllOfArray: []AllOf{ + { + ClusterName: "edge1", + //ClusterLabelName: "edge1", + }, + { + ClusterName: "edge2", + //ClusterLabelName: "edge2", + }, + { + AnyOfArray: []AnyOf{ + {ClusterLabelName: "east-us1"}, + {ClusterLabelName: "east-us2"}, + //{ClusterName: "east-us1"}, + //{ClusterName: "east-us2"}, + }, + }, + }, + + AnyOfArray: []AnyOf{}, + }, + }, + }, + + inputProject: "testProject", + inputCompositeApp: "testCompositeApp", + inputCompositeAppVersion: "testCompositeAppVersion", + inputGenericPlacementIntent: "testIntent", + expected: AppIntent{ + MetaData: MetaData{ + Name: "testAppIntent", + Description: "A sample AppIntent", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: SpecData{ + AppName: "SampleApp", + Intent: IntentStruc{ + AllOfArray: []AllOf{ + { + ClusterName: "edge1", + //ClusterLabelName: "edge1", + }, + { + ClusterName: "edge2", + //ClusterLabelName: "edge2", + }, + { + AnyOfArray: []AnyOf{ + {ClusterLabelName: "east-us1"}, + {ClusterLabelName: "east-us2"}, + //{ClusterName: "east-us1"}, + //{ClusterName: "east-us2"}, + }, + }, + }, + AnyOfArray: []AnyOf{}, + }, + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{\"project-name\":\"testProject\"," + + "\"description\":\"Test project for unit testing\"}"), + }, + CompositeAppKey{CompositeAppName: "testCompositeApp", + Version: "testCompositeAppVersion", Project: "testProject"}.String(): { + "compositeAppmetadata": []byte( + "{\"metadata\":{" + + "\"name\":\"testCompositeApp\"," + + "\"description\":\"description\"," + + "\"userData1\":\"user data\"," + + "\"userData2\":\"user data\"" + + "}," + + "\"spec\":{" + + "\"version\":\"version of the composite app\"}}"), + }, + GenericPlacementIntentKey{ + Name: "testIntent", + Project: "testProject", + CompositeApp: "testCompositeApp", + Version: "testCompositeAppVersion", + }.String(): { + "genericplacementintent": []byte( + "{\"metadata\":{\"Name\":\"testIntent\"," + + "\"Description\":\"A sample intent for testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}," + + "\"spec\":{\"Logical-Cloud\": \"logicalCloud1\"}}"), + }, + }, + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + appIntentCli := NewAppIntentClient() + got, err := appIntentCli.CreateAppIntent(testCase.inputAppIntent, testCase.inputProject, testCase.inputCompositeApp, testCase.inputCompositeAppVersion, testCase.inputGenericPlacementIntent) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("CreateAppIntent returned an unexpected error %s, ", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("CreateAppIntent returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("CreateAppIntent returned unexpected body: got %v; "+" expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetAppIntent(t *testing.T) { + testCases := []struct { + label string + expectedError string + expected AppIntent + mockdb *db.MockDB + appIntentName string + projectName string + compositeAppName string + compositeAppVersion string + genericPlacementIntent string + }{ + { + label: "Get Intent", + appIntentName: "testAppIntent", + projectName: "testProject", + compositeAppName: "testCompositeApp", + compositeAppVersion: "testCompositeAppVersion", + genericPlacementIntent: "testIntent", + expected: AppIntent{ + MetaData: MetaData{ + Name: "testAppIntent", + Description: "testAppIntent", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: SpecData{ + AppName: "SampleApp", + Intent: IntentStruc{ + AllOfArray: []AllOf{ + { + ClusterName: "edge1", + }, + { + ClusterName: "edge2", + }, + { + AnyOfArray: []AnyOf{ + {ClusterLabelName: "east-us1"}, + {ClusterLabelName: "east-us2"}, + }, + }, + }, + }, + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppIntentKey{ + Name: "testAppIntent", + Project: "testProject", + CompositeApp: "testCompositeApp", + Version: "testCompositeAppVersion", + Intent: "testIntent", + }.String(): { + "appintent": []byte( + "{\"metadata\":{\"Name\":\"testAppIntent\"," + + "\"Description\":\"testAppIntent\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}," + + "\"spec\":{\"app-name\": \"SampleApp\"," + + "\"intent\": {" + + "\"allOf\":[" + + "{\"cluster-name\":\"edge1\"}," + + "{\"cluster-name\":\"edge2\"}," + + "{" + + "\"anyOf\":[" + + "{" + + "\"cluster-label-name\":\"east-us1\"}," + + "{" + + "\"cluster-label-name\":\"east-us2\"}" + + "]}]" + + "}}}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + appIntentCli := NewAppIntentClient() + got, err := appIntentCli.GetAppIntent(testCase.appIntentName, testCase.projectName, testCase.compositeAppName, testCase.compositeAppVersion, + testCase.genericPlacementIntent) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("GetAppIntent returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("GetAppIntent returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("GetAppIntent returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + + }) + } +} diff --git a/src/orchestrator/pkg/module/app_profile.go b/src/orchestrator/pkg/module/app_profile.go new file mode 100644 index 00000000..77835fb4 --- /dev/null +++ b/src/orchestrator/pkg/module/app_profile.go @@ -0,0 +1,296 @@ +/* + * 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 ( + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// AppProfile contains the parameters needed for AppProfiles +// It implements the interface for managing the AppProfiles +type AppProfile struct { + Metadata AppProfileMetadata `json:"metadata"` + Spec AppProfileSpec `json:"spec"` +} + +type AppProfileContent struct { + Profile string `json:"profile"` +} + +// AppProfileMetadata contains the metadata for AppProfiles +type AppProfileMetadata struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +// AppProfileSpec contains the Spec for AppProfiles +type AppProfileSpec struct { + AppName string `json:"app-name"` +} + +// AppProfileKey is the key structure that is used in the database +type AppProfileKey struct { + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + CompositeAppVersion string `json:"compositeappversion"` + CompositeProfile string `json:"compositeprofile"` + Profile string `json:"profile"` +} + +type AppProfileQueryKey struct { + AppName string `json:"app-name"` +} + +type AppProfileFindByAppKey struct { + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + CompositeAppVersion string `json:"compositeappversion"` + CompositeProfile string `json:"compositeprofile"` + AppName string `json:"app-name"` +} + +// AppProfileManager exposes the AppProfile functionality +type AppProfileManager interface { + CreateAppProfile(provider, compositeApp, compositeAppVersion, compositeProfile string, ap AppProfile, ac AppProfileContent) (AppProfile, error) + GetAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, profile string) (AppProfile, error) + GetAppProfiles(project, compositeApp, compositeAppVersion, compositeProfile string) ([]AppProfile, error) + GetAppProfileByApp(project, compositeApp, compositeAppVersion, compositeProfile, appName string) (AppProfile, error) + GetAppProfileContent(project, compositeApp, compositeAppVersion, compositeProfile, profile string) (AppProfileContent, error) + GetAppProfileContentByApp(project, compositeApp, compositeAppVersion, compositeProfile, appName string) (AppProfileContent, error) + DeleteAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, profile string) error +} + +// AppProfileClient implements the Manager +// It will also be used to maintain some localized state +type AppProfileClient struct { + storeName string + tagMeta string + tagContent string +} + +// NewAppProfileClient returns an instance of the AppProfileClient +// which implements the Manager +func NewAppProfileClient() *AppProfileClient { + return &AppProfileClient{ + storeName: "orchestrator", + tagMeta: "profilemetadata", + tagContent: "profilecontent", + } +} + +// CreateAppProfile creates an entry for AppProfile in the database. +func (c *AppProfileClient) CreateAppProfile(project, compositeApp, compositeAppVersion, compositeProfile string, ap AppProfile, ac AppProfileContent) (AppProfile, error) { + key := AppProfileKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + Profile: ap.Metadata.Name, + } + qkey := AppProfileQueryKey{ + AppName: ap.Spec.AppName, + } + + res, err := c.GetAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, ap.Metadata.Name) + if res != (AppProfile{}) { + return AppProfile{}, pkgerrors.New("AppProfile already exists") + } + + res, err = c.GetAppProfileByApp(project, compositeApp, compositeAppVersion, compositeProfile, ap.Spec.AppName) + if res != (AppProfile{}) { + return AppProfile{}, pkgerrors.New("App already has an AppProfile") + } + + //Check if composite profile exists (success assumes existance of all higher level 'parent' objects) + _, err = NewCompositeProfileClient().GetCompositeProfile(compositeProfile, project, compositeApp, compositeAppVersion) + if err != nil { + return AppProfile{}, pkgerrors.New("Unable to find the project") + } + + // TODO: (after app api is ready) check that the app Spec.AppName exists as part of the composite app + + err = db.DBconn.Insert(c.storeName, key, qkey, c.tagMeta, ap) + if err != nil { + return AppProfile{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + err = db.DBconn.Insert(c.storeName, key, qkey, c.tagContent, ac) + if err != nil { + return AppProfile{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return ap, nil +} + +// GetAppProfile - return specified App Profile +func (c *AppProfileClient) GetAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, profile string) (AppProfile, error) { + key := AppProfileKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + Profile: profile, + } + + value, err := db.DBconn.Find(c.storeName, key, c.tagMeta) + if err != nil { + return AppProfile{}, pkgerrors.Wrap(err, "Get App Profile error") + } + + if value != nil { + ap := AppProfile{} + err = db.DBconn.Unmarshal(value[0], &ap) + if err != nil { + return AppProfile{}, pkgerrors.Wrap(err, "Unmarshalling AppProfile") + } + return ap, nil + } + + return AppProfile{}, pkgerrors.New("Error getting AppProfile") + +} + +// GetAppProfile - return all App Profiles for given composite profile +func (c *AppProfileClient) GetAppProfiles(project, compositeApp, compositeAppVersion, compositeProfile string) ([]AppProfile, error) { + + key := AppProfileKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + Profile: "", + } + + var resp []AppProfile + values, err := db.DBconn.Find(c.storeName, key, c.tagMeta) + if err != nil { + return []AppProfile{}, pkgerrors.Wrap(err, "Get AppProfiles") + } + + for _, value := range values { + ap := AppProfile{} + err = db.DBconn.Unmarshal(value, &ap) + if err != nil { + return []AppProfile{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + resp = append(resp, ap) + } + + return resp, nil +} + +// GetAppProfileByApp - return all App Profiles for given composite profile +func (c *AppProfileClient) GetAppProfileByApp(project, compositeApp, compositeAppVersion, compositeProfile, appName string) (AppProfile, error) { + + key := AppProfileFindByAppKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + AppName: appName, + } + + value, err := db.DBconn.Find(c.storeName, key, c.tagMeta) + if err != nil { + return AppProfile{}, pkgerrors.Wrap(err, "Get AppProfile by App") + } + + if value != nil { + ap := AppProfile{} + err = db.DBconn.Unmarshal(value[0], &ap) + if err != nil { + return AppProfile{}, pkgerrors.Wrap(err, "Unmarshalling AppProfile") + } + return ap, nil + } + + return AppProfile{}, pkgerrors.New("Error getting AppProfile by App") +} + +func (c *AppProfileClient) GetAppProfileContent(project, compositeApp, compositeAppVersion, compositeProfile, profile string) (AppProfileContent, error) { + key := AppProfileKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + Profile: profile, + } + + value, err := db.DBconn.Find(c.storeName, key, c.tagContent) + if err != nil { + return AppProfileContent{}, pkgerrors.Wrap(err, "Get Cluster Content") + } + + //value is a byte array + if value != nil { + ac := AppProfileContent{} + err = db.DBconn.Unmarshal(value[0], &ac) + if err != nil { + return AppProfileContent{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return ac, nil + } + + return AppProfileContent{}, pkgerrors.New("Error getting App Profile Content") +} + +func (c *AppProfileClient) GetAppProfileContentByApp(project, compositeApp, compositeAppVersion, compositeProfile, appName string) (AppProfileContent, error) { + key := AppProfileFindByAppKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + AppName: appName, + } + + value, err := db.DBconn.Find(c.storeName, key, c.tagContent) + if err != nil { + return AppProfileContent{}, pkgerrors.Wrap(err, "Get Cluster Content") + } + + //value is a byte array + if value != nil { + ac := AppProfileContent{} + err = db.DBconn.Unmarshal(value[0], &ac) + if err != nil { + return AppProfileContent{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return ac, nil + } + + return AppProfileContent{}, pkgerrors.New("Error getting App Profile Content") +} + +// Delete AppProfile from the database +func (c *AppProfileClient) DeleteAppProfile(project, compositeApp, compositeAppVersion, compositeProfile, profile string) error { + key := AppProfileKey{ + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + CompositeProfile: compositeProfile, + Profile: profile, + } + + err := db.DBconn.Remove(c.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete AppProfile entry;") + } + return nil +} diff --git a/src/orchestrator/pkg/module/composite_profile.go b/src/orchestrator/pkg/module/composite_profile.go new file mode 100644 index 00000000..dca2116a --- /dev/null +++ b/src/orchestrator/pkg/module/composite_profile.go @@ -0,0 +1,192 @@ +/* + * 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 ( + "encoding/json" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// CompositeProfile contains the parameters needed for CompositeProfiles +// It implements the interface for managing the CompositeProfiles +type CompositeProfile struct { + Metadata CompositeProfileMetadata `json:"metadata"` +} + +// CompositeProfileMetadata contains the metadata for CompositeProfiles +type CompositeProfileMetadata struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +// CompositeProfileKey is the key structure that is used in the database +type CompositeProfileKey struct { + Name string `json:"compositeprofile"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"compositeappversion"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (cpk CompositeProfileKey) String() string { + out, err := json.Marshal(cpk) + if err != nil { + return "" + } + + return string(out) +} + +// CompositeProfileManager exposes the CompositeProfile functionality +type CompositeProfileManager interface { + CreateCompositeProfile(cpf CompositeProfile, p string, ca string, + v string) (CompositeProfile, error) + GetCompositeProfile(compositeProfileName string, projectName string, + compositeAppName string, version string) (CompositeProfile, error) + GetCompositeProfiles(projectName string, compositeAppName string, + version string) ([]CompositeProfile, error) + DeleteCompositeProfile(compositeProfileName string, projectName string, + compositeAppName string, version string) error +} + +// CompositeProfileClient implements the Manager +// It will also be used to maintain some localized state +type CompositeProfileClient struct { + storeName string + tagMeta string +} + +// NewCompositeProfileClient returns an instance of the CompositeProfileClient +// which implements the Manager +func NewCompositeProfileClient() *CompositeProfileClient { + return &CompositeProfileClient{ + storeName: "orchestrator", + tagMeta: "compositeprofilemetadata", + } +} + +// CreateCompositeProfile creates an entry for CompositeProfile in the database. Other Input parameters for it - projectName, compositeAppName, version +func (c *CompositeProfileClient) CreateCompositeProfile(cpf CompositeProfile, p string, ca string, + v string) (CompositeProfile, error) { + + res, err := c.GetCompositeProfile(cpf.Metadata.Name, p, ca, v) + if res != (CompositeProfile{}) { + return CompositeProfile{}, pkgerrors.New("CompositeProfile already exists") + } + + //Check if project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return CompositeProfile{}, pkgerrors.New("Unable to find the project") + } + + // check if compositeApp exists + _, err = NewCompositeAppClient().GetCompositeApp(ca, v, p) + if err != nil { + return CompositeProfile{}, pkgerrors.New("Unable to find the composite-app") + } + + cProfkey := CompositeProfileKey{ + Name: cpf.Metadata.Name, + Project: p, + CompositeApp: ca, + Version: v, + } + + err = db.DBconn.Insert(c.storeName, cProfkey, nil, c.tagMeta, cpf) + if err != nil { + return CompositeProfile{}, pkgerrors.Wrap(err, "Create DB entry error") + } + + return cpf, nil +} + +// GetCompositeProfile shall take arguments - name of the composite profile, name of //// the project, name of the composite app and version of the composite app. It shall return the CompositeProfile if its present. +func (c *CompositeProfileClient) GetCompositeProfile(cpf string, p string, ca string, v string) (CompositeProfile, error) { + key := CompositeProfileKey{ + Name: cpf, + Project: p, + CompositeApp: ca, + Version: v, + } + + result, err := db.DBconn.Find(c.storeName, key, c.tagMeta) + if err != nil { + return CompositeProfile{}, pkgerrors.Wrap(err, "Get Composite Profile error") + } + + if result != nil { + cProf := CompositeProfile{} + err = db.DBconn.Unmarshal(result[0], &cProf) + if err != nil { + return CompositeProfile{}, pkgerrors.Wrap(err, "Unmarshalling CompositeProfile") + } + return cProf, nil + } + + return CompositeProfile{}, pkgerrors.New("Error getting CompositeProfile") +} + +// GetCompositeProfile shall take arguments - name of the composite profile, name of //// the project, name of the composite app and version of the composite app. It shall return the CompositeProfile if its present. +func (c *CompositeProfileClient) GetCompositeProfiles(p string, ca string, v string) ([]CompositeProfile, error) { + key := CompositeProfileKey{ + Name: "", + Project: p, + CompositeApp: ca, + Version: v, + } + + values, err := db.DBconn.Find(c.storeName, key, c.tagMeta) + if err != nil { + return []CompositeProfile{}, pkgerrors.Wrap(err, "Get Composite Profiles error") + } + + var resp []CompositeProfile + + for _, value := range values { + cp := CompositeProfile{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []CompositeProfile{}, pkgerrors.Wrap(err, "Get Composite Profiles unmarshalling error") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// DeleteCompositeProfile the intent from the database +func (c *CompositeProfileClient) DeleteCompositeProfile(cpf string, p string, ca string, v string) error { + key := CompositeProfileKey{ + Name: cpf, + Project: p, + CompositeApp: ca, + Version: v, + } + + err := db.DBconn.Remove(c.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete CompositeProfile entry;") + } + return nil +} diff --git a/src/orchestrator/pkg/module/composite_profile_test.go b/src/orchestrator/pkg/module/composite_profile_test.go new file mode 100644 index 00000000..af0dd7b7 --- /dev/null +++ b/src/orchestrator/pkg/module/composite_profile_test.go @@ -0,0 +1,175 @@ +/* + * 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 + +//pkgerrors "github.com/pkg/errors" + +/* TODO - db.MockDB needs to be enhanced and then these can be fixed up +func TestCreateCompositeProfile(t *testing.T) { + testCases := []struct { + label string + compositeProfile CompositeProfile + projectName string + compositeApp string + compositeAppVersion string + expectedError string + mockdb *db.MockDB + expected CompositeProfile + }{ + { + label: "Create CompositeProfile", + compositeProfile: CompositeProfile{ + Metadata: CompositeProfileMetadata{ + Name: "testCompositeProfile", + Description: "A sample Composite Profile for testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + projectName: "testProject", + compositeApp: "testCompositeApp", + compositeAppVersion: "v1", + expected: CompositeProfile{ + Metadata: CompositeProfileMetadata{ + Name: "testCompositeProfile", + Description: "A sample Composite Profile for 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", Project: "testProject", Version: "v1"}.String(): { + "compositeAppmetadata": []byte( + "{" + + "\"metadata\" : {" + + "\"Name\":\"testCompositeApp\"," + + "\"Description\":\"Test Composite App 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 + cprofCli := NewCompositeProfileClient() + got, err := cprofCli.CreateCompositeProfile(testCase.compositeProfile, testCase.projectName, testCase.compositeApp, testCase.compositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("CreateCompositeProfile returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("CreateCompositeProfile returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("CreateCompositeProfile returned unexpected body: got %v; "+" expected %v", got, testCase.expected) + } + } + }) + + } +} + +func TestGetCompositeProfile(t *testing.T) { + + testCases := []struct { + label string + expectedError string + expected CompositeProfile + mockdb *db.MockDB + compositeProfileName string + projectName string + compositeAppName string + compositeAppVersion string + }{ + { + label: "Get CompositeProfile", + compositeProfileName: "testCompositeProfile", + projectName: "testProject", + compositeAppName: "testCompositeApp", + compositeAppVersion: "v1", + expected: CompositeProfile{ + Metadata: CompositeProfileMetadata{ + Name: "testCompositeProfile", + Description: "A sample CompositeProfile for testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + CompositeProfileKey{ + Name: "testCompositeProfile", + Project: "testProject", + CompositeApp: "testCompositeApp", + Version: "v1", + }.String(): { + "compositeprofile": []byte( + "{\"metadata\":{\"Name\":\"testCompositeProfile\"," + + "\"Description\":\"A sample CompositeProfile for testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + cprofCli := NewCompositeProfileClient() + got, err := cprofCli.GetCompositeProfile(testCase.compositeProfileName, testCase.projectName, testCase.compositeAppName, testCase.compositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("GetCompositeProfile returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("GetCompositeProfile returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("GetCompositeProfile returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + + }) + } + +} +*/ diff --git a/src/orchestrator/pkg/module/deployment_intent_groups.go b/src/orchestrator/pkg/module/deployment_intent_groups.go new file mode 100644 index 00000000..fe622771 --- /dev/null +++ b/src/orchestrator/pkg/module/deployment_intent_groups.go @@ -0,0 +1,177 @@ +/* + * 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 ( + "encoding/json" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + "reflect" + + pkgerrors "github.com/pkg/errors" +) + +// DeploymentIntentGroup shall have 2 fields - MetaData and Spec +type DeploymentIntentGroup struct { + MetaData DepMetaData `json:"metadata"` + Spec DepSpecData `json:"spec"` +} + +// DepMetaData has Name, description, userdata1, userdata2 +type DepMetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +// DepSpecData has profile, version, OverrideValuesObj +type DepSpecData struct { + Profile string `json:"profile"` + Version string `json:"version"` + OverrideValuesObj []OverrideValues `json:"override-values"` +} + +// OverrideValues has appName and ValuesObj +type OverrideValues struct { + AppName string `json:"app-name"` + ValuesObj map[string]string `json:"values"` +} + +// Values has ImageRepository +// type Values struct { +// ImageRepository string `json:"imageRepository"` +// } + +// DeploymentIntentGroupManager is an interface which exposes the DeploymentIntentGroupManager functionality +type DeploymentIntentGroupManager interface { + CreateDeploymentIntentGroup(d DeploymentIntentGroup, p string, ca string, v string) (DeploymentIntentGroup, error) + GetDeploymentIntentGroup(di string, p string, ca string, v string) (DeploymentIntentGroup, error) + DeleteDeploymentIntentGroup(di string, p string, ca string, v string) error +} + +// DeploymentIntentGroupKey consists of Name of the deployment group, project name, CompositeApp name, CompositeApp version +type DeploymentIntentGroupKey struct { + Name string `json:"name"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"version"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (dk DeploymentIntentGroupKey) String() string { + out, err := json.Marshal(dk) + if err != nil { + return "" + } + return string(out) +} + +// DeploymentIntentGroupClient implements the DeploymentIntentGroupManager interface +type DeploymentIntentGroupClient struct { + storeName string + tagMetaData string +} + +// NewDeploymentIntentGroupClient return an instance of DeploymentIntentGroupClient which implements DeploymentIntentGroupManager +func NewDeploymentIntentGroupClient() *DeploymentIntentGroupClient { + return &DeploymentIntentGroupClient{ + storeName: "orchestrator", + tagMetaData: "deploymentintentgroup", + } +} + +// CreateDeploymentIntentGroup creates an entry for a given DeploymentIntentGroup in the database. Other Input parameters for it - projectName, compositeAppName, version +func (c *DeploymentIntentGroupClient) CreateDeploymentIntentGroup(d DeploymentIntentGroup, p string, ca string, + v string) (DeploymentIntentGroup, error) { + + res, err := c.GetDeploymentIntentGroup(d.MetaData.Name, p, ca, v) + if !reflect.DeepEqual(res, DeploymentIntentGroup{}) { + return DeploymentIntentGroup{}, pkgerrors.New("AppIntent already exists") + } + + //Check if project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return DeploymentIntentGroup{}, pkgerrors.New("Unable to find the project") + } + + //check if compositeApp exists + _, err = NewCompositeAppClient().GetCompositeApp(ca, v, p) + if err != nil { + return DeploymentIntentGroup{}, pkgerrors.New("Unable to find the composite-app") + } + + gkey := DeploymentIntentGroupKey{ + Name: d.MetaData.Name, + Project: p, + CompositeApp: ca, + Version: v, + } + + err = db.DBconn.Insert(c.storeName, gkey, nil, c.tagMetaData, d) + if err != nil { + return DeploymentIntentGroup{}, pkgerrors.Wrap(err, "Create DB entry error") + } + + return d, nil +} + +// GetDeploymentIntentGroup returns the DeploymentIntentGroup with a given name, project, compositeApp and version of compositeApp +func (c *DeploymentIntentGroupClient) GetDeploymentIntentGroup(di string, p string, ca string, v string) (DeploymentIntentGroup, error) { + + key := DeploymentIntentGroupKey{ + Name: di, + Project: p, + CompositeApp: ca, + Version: v, + } + + result, err := db.DBconn.Find(c.storeName, key, c.tagMetaData) + if err != nil { + return DeploymentIntentGroup{}, pkgerrors.Wrap(err, "Get DeploymentIntentGroup error") + } + + if result != nil { + d := DeploymentIntentGroup{} + err = db.DBconn.Unmarshal(result[0], &d) + if err != nil { + return DeploymentIntentGroup{}, pkgerrors.Wrap(err, "Unmarshalling DeploymentIntentGroup") + } + return d, nil + } + + return DeploymentIntentGroup{}, pkgerrors.New("Error getting DeploymentIntentGroup") + +} + +// DeleteDeploymentIntentGroup deletes a DeploymentIntentGroup +func (c *DeploymentIntentGroupClient) DeleteDeploymentIntentGroup(di string, p string, ca string, v string) error { + k := DeploymentIntentGroupKey{ + Name: di, + Project: p, + CompositeApp: ca, + Version: v, + } + + err := db.DBconn.Remove(c.storeName, k) + if err != nil { + return pkgerrors.Wrap(err, "Delete DeploymentIntentGroup entry;") + } + return nil + +} diff --git a/src/orchestrator/pkg/module/deployment_intent_groups_test.go b/src/orchestrator/pkg/module/deployment_intent_groups_test.go new file mode 100644 index 00000000..c0876ceb --- /dev/null +++ b/src/orchestrator/pkg/module/deployment_intent_groups_test.go @@ -0,0 +1,230 @@ +/* + * 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" +) + +func TestCreateDeploymentIntentGroup(t *testing.T) { + testCases := []struct { + label string + inputDeploymentIntentGrp DeploymentIntentGroup + inputProject string + inputCompositeApp string + inputCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected DeploymentIntentGroup + }{ + { + label: "Create DeploymentIntentGroup", + inputDeploymentIntentGrp: DeploymentIntentGroup{ + MetaData: DepMetaData{ + Name: "testDeploymentIntentGroup", + Description: "DescriptionTestDeploymentIntentGroup", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: DepSpecData{ + Profile: "Testprofile", + Version: "version of deployment", + OverrideValuesObj: []OverrideValues{ + {AppName: "TestAppName", + ValuesObj: map[string]string{ + "imageRepository": "registry.hub.docker.com", + }}, + {AppName: "TestAppName", + ValuesObj: map[string]string{ + "imageRepository": "registry.hub.docker.com", + }}, + }, + }, + }, + inputProject: "testProject", + inputCompositeApp: "testCompositeApp", + inputCompositeAppVersion: "testCompositeAppVersion", + expected: DeploymentIntentGroup{ + MetaData: DepMetaData{ + Name: "testDeploymentIntentGroup", + Description: "DescriptionTestDeploymentIntentGroup", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: DepSpecData{ + Profile: "Testprofile", + Version: "version of deployment", + OverrideValuesObj: []OverrideValues{ + {AppName: "TestAppName", + ValuesObj: map[string]string{ + "imageRepository": "registry.hub.docker.com", + }}, + {AppName: "TestAppName", + ValuesObj: map[string]string{ + "imageRepository": "registry.hub.docker.com", + }}, + }, + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{\"project-name\":\"testProject\"," + + "\"description\":\"Test project for unit testing\"}"), + }, + CompositeAppKey{CompositeAppName: "testCompositeApp", + Version: "testCompositeAppVersion", Project: "testProject"}.String(): { + "compositeAppmetadata": []byte( + "{\"metadata\":{" + + "\"name\":\"testCompositeApp\"," + + "\"description\":\"description\"," + + "\"userData1\":\"user data\"," + + "\"userData2\":\"user data\"" + + "}," + + "\"spec\":{" + + "\"version\":\"version of the composite app\"}}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + depIntentCli := NewDeploymentIntentGroupClient() + got, err := depIntentCli.CreateDeploymentIntentGroup(testCase.inputDeploymentIntentGrp, testCase.inputProject, testCase.inputCompositeApp, testCase.inputCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("CreateDeploymentIntentGroup returned an unexpected error %s, ", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("CreateDeploymentIntentGroup returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("CreateDeploymentIntentGroup returned unexpected body: got %v; "+" expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetDeploymentIntentGroup(t *testing.T) { + testCases := []struct { + label string + inputDeploymentIntentGrp string + inputProject string + inputCompositeApp string + inputCompositeAppVersion string + expected DeploymentIntentGroup + expectedError string + mockdb *db.MockDB + }{ + { + label: "Get DeploymentIntentGroup", + inputDeploymentIntentGrp: "testDeploymentIntentGroup", + inputProject: "testProject", + inputCompositeApp: "testCompositeApp", + inputCompositeAppVersion: "testCompositeAppVersion", + expected: DeploymentIntentGroup{ + MetaData: DepMetaData{ + Name: "testDeploymentIntentGroup", + Description: "DescriptionTestDeploymentIntentGroup", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: DepSpecData{ + Profile: "Testprofile", + Version: "version of deployment", + OverrideValuesObj: []OverrideValues{ + {AppName: "TestAppName", + ValuesObj: map[string]string{ + "imageRepository": "registry.hub.docker.com", + }}, + {AppName: "TestAppName", + ValuesObj: map[string]string{ + "imageRepository": "registry.hub.docker.com", + }}, + }, + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + DeploymentIntentGroupKey{ + Name: "testDeploymentIntentGroup", + Project: "testProject", + CompositeApp: "testCompositeApp", + Version: "testCompositeAppVersion", + }.String(): { + "deploymentintentgroup": []byte( + "{\"metadata\":{\"name\":\"testDeploymentIntentGroup\"," + + "\"description\":\"DescriptionTestDeploymentIntentGroup\"," + + "\"userData1\": \"userData1\"," + + "\"userData2\": \"userData2\"}," + + "\"spec\":{\"profile\": \"Testprofile\"," + + "\"version\": \"version of deployment\"," + + "\"override-values\":[" + + "{" + + "\"app-name\": \"TestAppName\"," + + "\"values\": " + + "{" + + "\"imageRepository\":\"registry.hub.docker.com\"" + + "}" + + "}," + + "{" + + "\"app-name\": \"TestAppName\"," + + "\"values\": " + + "{" + + "\"imageRepository\":\"registry.hub.docker.com\"" + + "}" + + "}" + + "]}}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + depIntentCli := NewDeploymentIntentGroupClient() + got, err := depIntentCli.GetDeploymentIntentGroup(testCase.inputDeploymentIntentGrp, testCase.inputProject, testCase.inputCompositeApp, testCase.inputCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("GetDeploymentIntentGroup returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("GetDeploymentIntentGroup returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("GetDeploymentIntentGroup returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} diff --git a/src/orchestrator/pkg/module/generic_placement_intent.go b/src/orchestrator/pkg/module/generic_placement_intent.go new file mode 100644 index 00000000..8e739fc8 --- /dev/null +++ b/src/orchestrator/pkg/module/generic_placement_intent.go @@ -0,0 +1,165 @@ +/* + * 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 ( + "encoding/json" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// GenericPlacementIntent shall have 2 fields - metadata and spec +type GenericPlacementIntent struct { + MetaData GenIntentMetaData `json:"metadata"` + Spec GenIntentSpecData `json:"spec"` +} + +// GenIntentMetaData has name, description, userdata1, userdata2 +type GenIntentMetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +// GenIntentSpecData has logical-cloud-name +type GenIntentSpecData struct { + LogicalCloud string `json:"logical-cloud"` +} + +// GenericPlacementIntentManager is an interface which exposes the GenericPlacementIntentManager functionality +type GenericPlacementIntentManager interface { + CreateGenericPlacementIntent(g GenericPlacementIntent, p string, ca string, + v string) (GenericPlacementIntent, error) + GetGenericPlacementIntent(intentName string, projectName string, + compositeAppName string, version string) (GenericPlacementIntent, error) + DeleteGenericPlacementIntent(intentName string, projectName string, + compositeAppName string, version string) error +} + +// GenericPlacementIntentKey is used as the primary key +type GenericPlacementIntentKey struct { + Name string `json:"name"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"version"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (gk GenericPlacementIntentKey) String() string { + out, err := json.Marshal(gk) + if err != nil { + return "" + } + return string(out) +} + +// GenericPlacementIntentClient implements the GenericPlacementIntentManager interface +type GenericPlacementIntentClient struct { + storeName string + tagMetaData string +} + +// NewGenericPlacementIntentClient return an instance of GenericPlacementIntentClient which implements GenericPlacementIntentManager +func NewGenericPlacementIntentClient() *GenericPlacementIntentClient { + return &GenericPlacementIntentClient{ + storeName: "orchestrator", + tagMetaData: "genericplacementintent", + } +} + +// CreateGenericPlacementIntent creates an entry for GenericPlacementIntent in the database. Other Input parameters for it - projectName, compositeAppName, version +func (c *GenericPlacementIntentClient) CreateGenericPlacementIntent(g GenericPlacementIntent, p string, ca string, + v string) (GenericPlacementIntent, error) { + + // check if the genericPlacement already exists. + res, err := c.GetGenericPlacementIntent(g.MetaData.Name, p, ca, v) + if res != (GenericPlacementIntent{}) { + return GenericPlacementIntent{}, pkgerrors.New("Intent already exists") + } + + //Check if project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return GenericPlacementIntent{}, pkgerrors.New("Unable to find the project") + } + + // check if compositeApp exists + _, err = NewCompositeAppClient().GetCompositeApp(ca, v, p) + if err != nil { + return GenericPlacementIntent{}, pkgerrors.New("Unable to find the composite-app") + } + + gkey := GenericPlacementIntentKey{ + Name: g.MetaData.Name, + Project: p, + CompositeApp: ca, + Version: v, + } + + err = db.DBconn.Insert(c.storeName, gkey, nil, c.tagMetaData, g) + if err != nil { + return GenericPlacementIntent{}, pkgerrors.Wrap(err, "Create DB entry error") + } + + return g, nil +} + +// GetGenericPlacementIntent shall take arguments - name of the intent, name of the project, name of the composite app and version of the composite app. It shall return the genericPlacementIntent if its present. +func (c *GenericPlacementIntentClient) GetGenericPlacementIntent(i string, p string, ca string, v string) (GenericPlacementIntent, error) { + key := GenericPlacementIntentKey{ + Name: i, + Project: p, + CompositeApp: ca, + Version: v, + } + + result, err := db.DBconn.Find(c.storeName, key, c.tagMetaData) + if err != nil { + return GenericPlacementIntent{}, pkgerrors.Wrap(err, "Get Intent error") + } + + if result != nil { + g := GenericPlacementIntent{} + err = db.DBconn.Unmarshal(result[0], &g) + if err != nil { + return GenericPlacementIntent{}, pkgerrors.Wrap(err, "Unmarshalling GenericPlacement Intent") + } + return g, nil + } + + return GenericPlacementIntent{}, pkgerrors.New("Error getting GenericPlacementIntent") + +} + +// DeleteGenericPlacementIntent the intent from the database +func (c *GenericPlacementIntentClient) DeleteGenericPlacementIntent(i string, p string, ca string, v string) error { + key := GenericPlacementIntentKey{ + Name: i, + Project: p, + CompositeApp: ca, + Version: v, + } + + err := db.DBconn.Remove(c.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete Project entry;") + } + return nil +} diff --git a/src/orchestrator/pkg/module/generic_placement_intent_test.go b/src/orchestrator/pkg/module/generic_placement_intent_test.go new file mode 100644 index 00000000..c87f9ddc --- /dev/null +++ b/src/orchestrator/pkg/module/generic_placement_intent_test.go @@ -0,0 +1,184 @@ +/* + * 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" +) + +func TestCreateGenericPlacementIntent(t *testing.T) { + testCases := []struct { + label string + inputIntent GenericPlacementIntent + inputProject string + inputCompositeApp string + inputCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected GenericPlacementIntent + }{ + { + label: "Create GenericPlacementIntent", + inputIntent: GenericPlacementIntent{ + MetaData: GenIntentMetaData{ + Name: "testGenericPlacement", + Description: " A sample intent for testing", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: GenIntentSpecData{ + LogicalCloud: "logicalCloud1", + }, + }, + inputProject: "testProject", + inputCompositeApp: "testCompositeApp", + inputCompositeAppVersion: "testCompositeAppVersion", + expected: GenericPlacementIntent{ + MetaData: GenIntentMetaData{ + Name: "testGenericPlacement", + Description: " A sample intent for testing", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: GenIntentSpecData{ + LogicalCloud: "logicalCloud1", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{\"project-name\":\"testProject\"," + + "\"description\":\"Test project for unit testing\"}"), + }, + CompositeAppKey{CompositeAppName: "testCompositeApp", + Version: "testCompositeAppVersion", Project: "testProject"}.String(): { + "compositeAppmetadata": []byte( + "{\"metadata\":{" + + "\"name\":\"testCompositeApp\"," + + "\"description\":\"description\"," + + "\"userData1\":\"user data\"," + + "\"userData2\":\"user data\"" + + "}," + + "\"spec\":{" + + "\"version\":\"version of the composite app\"}}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + intentCli := NewGenericPlacementIntentClient() + got, err := intentCli.CreateGenericPlacementIntent(testCase.inputIntent, testCase.inputProject, testCase.inputCompositeApp, testCase.inputCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("CreateGenericPlacementIntent returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("CreateGenericPlacementIntent returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("CreateGenericPlacementIntent returned unexpected body: got %v; "+" expected %v", got, testCase.expected) + } + } + }) + + } +} + +func TestGetGenericPlacementIntent(t *testing.T) { + + testCases := []struct { + label string + expectedError string + expected GenericPlacementIntent + mockdb *db.MockDB + intentName string + projectName string + compositeAppName string + compositeAppVersion string + }{ + { + label: "Get Intent", + intentName: "testIntent", + projectName: "testProject", + compositeAppName: "testCompositeApp", + compositeAppVersion: "testVersion", + expected: GenericPlacementIntent{ + MetaData: GenIntentMetaData{ + Name: "testIntent", + Description: "A sample intent for testing", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: GenIntentSpecData{ + LogicalCloud: "logicalCloud1", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + GenericPlacementIntentKey{ + Name: "testIntent", + Project: "testProject", + CompositeApp: "testCompositeApp", + Version: "testVersion", + }.String(): { + "genericplacementintent": []byte( + "{\"metadata\":{\"Name\":\"testIntent\"," + + "\"Description\":\"A sample intent for testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}," + + "\"spec\":{\"Logical-Cloud\": \"logicalCloud1\"}}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + intentCli := NewGenericPlacementIntentClient() + got, err := intentCli.GetGenericPlacementIntent(testCase.intentName, testCase.projectName, testCase.compositeAppName, testCase.compositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("GetGenericPlacementIntent returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("GetGenericPlacementIntent returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("GetGenericPlacementIntent returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + + }) + } + +} diff --git a/src/orchestrator/pkg/module/module.go b/src/orchestrator/pkg/module/module.go index f80ff55b..8f2948dd 100644 --- a/src/orchestrator/pkg/module/module.go +++ b/src/orchestrator/pkg/module/module.go @@ -18,10 +18,16 @@ package module // Client for using the services in the orchestrator type Client struct { - Project *ProjectClient - CompositeApp *CompositeAppClient - Controller *ControllerClient - Cluster *ClusterClient + Project *ProjectClient + CompositeApp *CompositeAppClient + Controller *ControllerClient + Cluster *ClusterClient + GenericPlacementIntent *GenericPlacementIntentClient + AppIntent *AppIntentClient + DeploymentIntentGroup *DeploymentIntentGroupClient + Intent *IntentClient + CompositeProfile *CompositeProfileClient + AppProfile *AppProfileClient // Add Clients for API's here } @@ -32,6 +38,12 @@ func NewClient() *Client { c.CompositeApp = NewCompositeAppClient() c.Controller = NewControllerClient() c.Cluster = NewClusterClient() + c.GenericPlacementIntent = NewGenericPlacementIntentClient() + c.AppIntent = NewAppIntentClient() + c.DeploymentIntentGroup = NewDeploymentIntentGroupClient() + c.Intent = NewIntentClient() + c.CompositeProfile = NewCompositeProfileClient() + c.AppProfile = NewAppProfileClient() // Add Client API handlers here return c } diff --git a/src/orchestrator/utils/utils.go b/src/orchestrator/utils/utils.go new file mode 100644 index 00000000..44cf5120 --- /dev/null +++ b/src/orchestrator/utils/utils.go @@ -0,0 +1,66 @@ +/* + * 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 utils + +import ( + "archive/tar" + "compress/gzip" + "io" + + pkgerrors "github.com/pkg/errors" +) + +func IsTarGz(r io.Reader) error { + //Check if it is a valid gz + gzf, err := gzip.NewReader(r) + if err != nil { + return pkgerrors.Wrap(err, "Invalid gzip format") + } + + //Check if it is a valid tar file + //Unfortunately this can only be done by inspecting all the tar contents + tarR := tar.NewReader(gzf) + first := true + + for true { + header, err := tarR.Next() + + if err == io.EOF { + //Check if we have just a gzip file without a tar archive inside + if first { + return pkgerrors.New("Empty or non-existant Tar file found") + } + //End of archive + break + } + + if err != nil { + return pkgerrors.Errorf("Error reading tar file %s", err.Error()) + } + + //Check if files are of type directory and regular file + if header.Typeflag != tar.TypeDir && + header.Typeflag != tar.TypeReg { + return pkgerrors.Errorf("Unknown header in tar %s, %s", + header.Name, string(header.Typeflag)) + } + + first = false + } + + return nil +} diff --git a/src/orchestrator/utils/utils_test.go b/src/orchestrator/utils/utils_test.go new file mode 100644 index 00000000..63230e49 --- /dev/null +++ b/src/orchestrator/utils/utils_test.go @@ -0,0 +1,74 @@ +/* + * Copyright 2018 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 utils + +import ( + "bytes" + "testing" +) + +func TestIsTarGz(t *testing.T) { + + t.Run("Valid tar.gz", func(t *testing.T) { + content := []byte{ + 0x1f, 0x8b, 0x08, 0x08, 0xb0, 0x6b, 0xf4, 0x5b, + 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74, + 0x61, 0x72, 0x00, 0xed, 0xce, 0x41, 0x0a, 0xc2, + 0x30, 0x10, 0x85, 0xe1, 0xac, 0x3d, 0x45, 0x4e, + 0x50, 0x12, 0xd2, 0xc4, 0xe3, 0x48, 0xa0, 0x01, + 0x4b, 0x52, 0x0b, 0xed, 0x88, 0x1e, 0xdf, 0x48, + 0x11, 0x5c, 0x08, 0xa5, 0x8b, 0x52, 0x84, 0xff, + 0xdb, 0xbc, 0x61, 0x66, 0x16, 0x4f, 0xd2, 0x2c, + 0x8d, 0x3c, 0x45, 0xed, 0xc8, 0x54, 0x21, 0xb4, + 0xef, 0xb4, 0x67, 0x6f, 0xbe, 0x73, 0x61, 0x9d, + 0xb2, 0xce, 0xd5, 0x55, 0xf0, 0xde, 0xd7, 0x3f, + 0xdb, 0xd6, 0x49, 0x69, 0xb3, 0x67, 0xa9, 0x8f, + 0xfb, 0x2c, 0x71, 0xd2, 0x5a, 0xc5, 0xee, 0x92, + 0x73, 0x8e, 0x43, 0x7f, 0x4b, 0x3f, 0xff, 0xd6, + 0xee, 0x7f, 0xea, 0x9a, 0x4a, 0x19, 0x1f, 0xe3, + 0x54, 0xba, 0xd3, 0xd1, 0x55, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1b, 0xbc, 0x00, 0xb5, 0xe8, + 0x4a, 0xf9, 0x00, 0x28, 0x00, 0x00, + } + + err := IsTarGz(bytes.NewBuffer(content)) + if err != nil { + t.Errorf("Error reading valid tar.gz file %s", err.Error()) + } + }) + + t.Run("Invalid tar.gz", func(t *testing.T) { + content := []byte{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xf2, 0x48, 0xcd, + } + + err := IsTarGz(bytes.NewBuffer(content)) + if err == nil { + t.Errorf("Error should NOT be nil") + } + }) + + t.Run("Empty tar.gz", func(t *testing.T) { + content := []byte{} + err := IsTarGz(bytes.NewBuffer(content)) + if err == nil { + t.Errorf("Error should NOT be nil") + } + }) +} |