Provide OverrideParameters capability for infra_workload API 55/100055/6
authorKonrad Bańka <k.banka@samsung.com>
Tue, 7 Jan 2020 14:34:20 +0000 (15:34 +0100)
committerKonrad Bańka <k.banka@samsung.com>
Thu, 20 Feb 2020 09:47:38 +0000 (10:47 +0100)
Provide parameters provided in sdnc, oof and user directives
as override parameters for instantiate call.

Issue-ID: MULTICLOUD-838
Signed-off-by: Konrad Bańka <k.banka@samsung.com>
Change-Id: I69c6dab82ee233b2365a656bfaf1c7d64e375c98

src/k8splugin/api/brokerhandler.go
src/k8splugin/api/brokerhandler_test.go
src/k8splugin/internal/helm/helm_test.go

index 7671db4..c98e1c4 100644 (file)
@@ -16,11 +16,11 @@ package api
 import (
        "encoding/json"
        "io"
-       "log"
        "net/http"
 
        "github.com/onap/multicloud-k8s/src/k8splugin/internal/app"
        "github.com/onap/multicloud-k8s/src/k8splugin/internal/helm"
+       log "github.com/onap/multicloud-k8s/src/k8splugin/internal/logutils"
 
        "github.com/gorilla/mux"
 )
@@ -33,16 +33,34 @@ type brokerInstanceHandler struct {
 }
 
 type brokerRequest struct {
-       GenericVnfID                 string                 `json:"generic-vnf-id"`
-       VFModuleID                   string                 `json:"vf-module-id"`
-       VFModuleModelInvariantID     string                 `json:"vf-module-model-invariant-id"`
-       VFModuleModelVersionID       string                 `json:"vf-module-model-version-id"`
-       VFModuleModelCustomizationID string                 `json:"vf-module-model-customization-id"`
-       OOFDirectives                map[string]interface{} `json:"oof_directives"`
-       SDNCDirectives               map[string]interface{} `json:"sdnc_directives"`
-       UserDirectives               map[string]interface{} `json:"user_directives"`
-       TemplateType                 string                 `json:"template_type"`
-       TemplateData                 map[string]interface{} `json:"template_data"`
+       GenericVnfID                 string       `json:"generic-vnf-id"`
+       VFModuleID                   string       `json:"vf-module-id"`
+       VFModuleModelInvariantID     string       `json:"vf-module-model-invariant-id"`
+       VFModuleModelVersionID       string       `json:"vf-module-model-version-id"`
+       VFModuleModelCustomizationID string       `json:"vf-module-model-customization-id"`
+       OOFDirectives                directive    `json:"oof_directives"`
+       SDNCDirectives               directive    `json:"sdnc_directives"`
+       UserDirectives               directive    `json:"user_directives"`
+       TemplateType                 string       `json:"template_type"`
+       TemplateData                 templateData `json:"template_data"`
+}
+
+type directive struct {
+       Attributes []attribute `json:"attributes"`
+}
+
+type attribute struct {
+       Key   string `json:"attribute_name"`
+       Value string `json:"attribute_value"`
+}
+
+type templateData struct {
+       StackName       string `json:"stack_name"` //Only this property is relevant (exported)
+       disableRollback string `json:"disable_rollback"`
+       environment     string `json:"environment"`
+       parameters      string `json:"parameters"`
+       template        string `json:"template"`
+       timeoutMins     string `json:"timeout_mins"`
 }
 
 type brokerPOSTResponse struct {
@@ -67,50 +85,16 @@ type brokerDELETEResponse struct {
        WorkloadStatusReason map[string]interface{} `json:"workload_status_reason"`
 }
 
-// getUserDirectiveValue parses the following kind of json
-// "user_attributes": {
-//             "attributes": [
-//             {
-//                     "attribute_value": "foo",
-//                     "attribute_name": "bar"
-//             },
-//             {
-//                     "attribute_value": "value2",
-//                     "attribute_name": "name2"
-//             }
-//             ]
-// }
-func (b brokerRequest) getAttributeValue(directives map[string]interface{}, inp string) string {
-       attributes, ok := directives["attributes"].([]interface{})
-       if !ok {
-               log.Println("Unable to cast attributes to []interface{}")
-               return ""
-       }
-
-       for _, value := range attributes {
-
-               attribute, ok := value.(map[string]interface{})
-               if !ok {
-                       log.Println("Unable to cast attribute to map[string]interface{}")
-                       return ""
-               }
-
-               attributeName, ok := attribute["attribute_name"].(string)
-               if !ok {
-                       log.Println("Unable to cast attribute_name to string")
-                       return ""
-               }
-               if attributeName == inp {
-                       attributevalue, ok := attribute["attribute_value"].(string)
-                       if !ok {
-                               log.Println("Unable to cast attribute_value to string")
-                               return ""
-                       }
-
-                       return attributevalue
+// Convert directives stored in broker request to map[string]string format with
+// merge including precedence provided
+func (b brokerRequest) convertDirectives() map[string]string {
+       extractedAttributes := make(map[string]string)
+       for _, section := range [3]directive{b.SDNCDirectives, b.OOFDirectives, b.UserDirectives} {
+               for _, attribute := range section.Attributes {
+                       extractedAttributes[attribute.Key] = attribute.Value
                }
        }
-       return ""
+       return extractedAttributes
 }
 
 func (b brokerInstanceHandler) createHandler(w http.ResponseWriter, r *http.Request) {
@@ -119,6 +103,10 @@ func (b brokerInstanceHandler) createHandler(w http.ResponseWriter, r *http.Requ
 
        var req brokerRequest
        err := json.NewDecoder(r.Body).Decode(&req)
+       log.Info("Broker API Payload", log.Fields{
+               "Body":    r.Body,
+               "Payload": req,
+       })
        switch {
        case err == io.EOF:
                http.Error(w, "Body empty", http.StatusBadRequest)
@@ -144,15 +132,24 @@ func (b brokerInstanceHandler) createHandler(w http.ResponseWriter, r *http.Requ
                return
        }
 
-       profileName := req.getAttributeValue(req.SDNCDirectives, "k8s-rb-profile-name")
-       if profileName == "" {
-               http.Error(w, "k8s-rb-profile-name is missing from sdnc-directives", http.StatusBadRequest)
+       if req.GenericVnfID == "" {
+               http.Error(w, "generic-vnf-id is empty", http.StatusBadRequest)
+               return
+       }
+       if req.VFModuleID == "" {
+               http.Error(w, "vf-module-id is empty", http.StatusBadRequest)
+               return
+       }
+
+       if req.TemplateData.StackName == "" {
+               http.Error(w, "stack_name is missing from template_data", http.StatusBadRequest)
                return
        }
 
-       vfModuleName := req.getAttributeValue(req.SDNCDirectives, "vf_module_name")
-       if vfModuleName == "" {
-               http.Error(w, "vf_module_name is missing from sdnc-directives", http.StatusBadRequest)
+       directives := req.convertDirectives()
+       profileName, ok := directives["k8s-rb-profile-name"]
+       if !ok {
+               http.Error(w, "k8s-rb-profile-name is missing from directives", http.StatusBadRequest)
                return
        }
 
@@ -163,9 +160,13 @@ func (b brokerInstanceHandler) createHandler(w http.ResponseWriter, r *http.Requ
        instReq.ProfileName = profileName
        instReq.CloudRegion = cloudRegion
        instReq.Labels = map[string]string{
-               "vf_module_name": vfModuleName,
+               "stack-name": req.TemplateData.StackName,
        }
+       instReq.OverrideValues = directives
 
+       log.Info("Instance API Payload", log.Fields{
+               "payload": instReq,
+       })
        resp, err := b.client.Create(instReq)
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -178,6 +179,9 @@ func (b brokerInstanceHandler) createHandler(w http.ResponseWriter, r *http.Requ
                TemplateResponse: resp.Resources,
                WorkloadStatus:   "CREATE_COMPLETE",
        }
+       log.Info("Broker API Response", log.Fields{
+               "response": brokerResp,
+       })
 
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
@@ -205,6 +209,9 @@ func (b brokerInstanceHandler) getHandler(w http.ResponseWriter, r *http.Request
                WorkloadStatus: "CREATE_COMPLETE",
        }
 
+       log.Info("Broker API Response", log.Fields{
+               "response": brokerResp,
+       })
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        err = json.NewEncoder(w).Encode(brokerResp)
@@ -214,12 +221,12 @@ func (b brokerInstanceHandler) getHandler(w http.ResponseWriter, r *http.Request
        }
 }
 
-// getHandler retrieves information about an instance via the ID
+// findHandler retrieves information about an instance via the ID
 func (b brokerInstanceHandler) findHandler(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)
-       //name is an alias for vf_module_name from the so adapter
+       //name is an alias for stack-name from the so adapter
        name := vars["name"]
-       responses, _ := b.client.Find("", "", "", map[string]string{"vf_module_name": name})
+       responses, _ := b.client.Find("", "", "", map[string]string{"stack-name": name})
 
        brokerResp := brokerGETResponse{
                TemplateType:   "heat",
@@ -244,6 +251,9 @@ func (b brokerInstanceHandler) findHandler(w http.ResponseWriter, r *http.Reques
                }
        }
 
+       log.Info("Broker API Response", log.Fields{
+               "response": brokerResp,
+       })
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        err := json.NewEncoder(w).Encode(brokerResp)
@@ -269,6 +279,9 @@ func (b brokerInstanceHandler) deleteHandler(w http.ResponseWriter, r *http.Requ
                WorkloadID:     instanceID,
                WorkloadStatus: "DELETE_COMPLETE",
        }
+       log.Info("Broker API Response", log.Fields{
+               "response": brokerResp,
+       })
 
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusAccepted)
index 83ff588..c822f6d 100644 (file)
@@ -3,7 +3,7 @@ Copyright 2018 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
+       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.
@@ -21,6 +21,7 @@ import (
        "net/http"
        "net/http/httptest"
        "reflect"
+       "strings"
        "testing"
 
        "github.com/onap/multicloud-k8s/src/k8splugin/internal/app"
@@ -30,13 +31,114 @@ import (
        "k8s.io/apimachinery/pkg/runtime/schema"
 )
 
+func TestConvertDirectives(t *testing.T) {
+       testCases := []struct {
+               label    string
+               input    brokerRequest
+               expected map[string]string
+       }{
+               {
+                       label:    "Single variable",
+                       expected: map[string]string{"test": "true"},
+                       input: brokerRequest{SDNCDirectives: directive{[]attribute{{
+                               Key:   "test",
+                               Value: "true",
+                       }}}},
+               },
+               {
+                       label:    "Empty parameter",
+                       expected: map[string]string{"test": ""},
+                       input: brokerRequest{OOFDirectives: directive{[]attribute{{
+                               Key:   "test",
+                               Value: "",
+                       }}}},
+               },
+               {
+                       label:    "Null entry",
+                       input:    brokerRequest{},
+                       expected: make(map[string]string),
+               },
+               {
+                       label: "Complex helm overrides",
+                       /*
+                               String with int will be later treated as int in helm.TemplateClient
+                               (see helm/pkg/strvals/parser.go)
+                               If unsure, convert type in helm chart like `{{ toString $value }}` or `{{ int $value }}`
+                               (see http://masterminds.github.io/sprig/conversion.html)
+                       */
+                       expected: map[string]string{"name": "{a, b, c}", "servers[0].port": "80"},
+                       input: brokerRequest{UserDirectives: directive{[]attribute{
+                               {
+                                       Key:   "name",
+                                       Value: "{a, b, c}",
+                               },
+                               {
+                                       Key:   "servers[0].port",
+                                       Value: "80",
+                               },
+                       }}},
+               },
+               {
+                       label:    "Override variables",
+                       expected: map[string]string{"empty": "", "sdnc": "sdnc", "user": "user", "oof": "oof"},
+                       input: brokerRequest{
+                               SDNCDirectives: directive{[]attribute{
+                                       {
+                                               Key:   "empty",
+                                               Value: "sdnc",
+                                       },
+                                       {
+                                               Key:   "sdnc",
+                                               Value: "sdnc",
+                                       },
+                                       {
+                                               Key:   "oof",
+                                               Value: "sdnc",
+                                       },
+                               }},
+                               OOFDirectives: directive{[]attribute{
+                                       {
+                                               Key:   "oof",
+                                               Value: "oof",
+                                       },
+                                       {
+                                               Key:   "user",
+                                               Value: "oof",
+                                       },
+                               }},
+                               UserDirectives: directive{[]attribute{
+                                       {
+                                               Key:   "user",
+                                               Value: "user",
+                                       },
+                                       {
+                                               Key:   "empty",
+                                               Value: "",
+                                       },
+                               }},
+                       },
+               },
+       }
+
+       for _, testCase := range testCases {
+               t.Run(testCase.label, func(t *testing.T) {
+                       result := testCase.input.convertDirectives()
+                       if !reflect.DeepEqual(result, testCase.expected) {
+                               t.Fatalf("Unexpected result. Wanted '%v', retrieved '%v'",
+                                       testCase.expected, result)
+                       }
+               })
+       }
+}
+
 func TestBrokerCreateHandler(t *testing.T) {
        testCases := []struct {
-               label        string
-               input        io.Reader
-               expected     brokerPOSTResponse
-               expectedCode int
-               instClient   *mockInstanceClient
+               label         string
+               input         io.Reader
+               expected      brokerPOSTResponse
+               expectedError string
+               expectedCode  int
+               instClient    *mockInstanceClient
        }{
                {
                        label:        "Missing body failure",
@@ -48,41 +150,89 @@ func TestBrokerCreateHandler(t *testing.T) {
                        expectedCode: http.StatusUnprocessableEntity,
                },
                {
-                       label: "Missing vf-module-*-id parameter",
+                       label:         "Missing vf-module-*-id parameter",
+                       expectedError: "vf-module-model-customization-id is empty",
+                       expectedCode:  http.StatusBadRequest,
                        input: bytes.NewBuffer([]byte(`{
-                               "vf-module-model-customization-id": "84sdfkio938",
                                "vf-module-model-invariant-id": "123456qwerty",
+                               "vf-module-model-version-id": "123qweasdzxc",
+                               "generic-vnf-id": "dummy-vnf-id",
+                               "vf-module-id": "dummy-vfm-id",
+                               "template_data": {
+                                       "stack_name": "dummy-stack-name"
+                               },
                                "sdnc_directives": {
                                        "attributes": [
                                                {
-                                                       "attribute_name": "vf_module_name",
-                                                       "attribute_value": "test-vf-module-name"
-                                               },
+                                                       "attribute_name": "k8s-rb-profile-name",
+                                                       "attribute_value": "dummy-profile"
+                                               }
+                                       ]
+                               }
+                       }`)),
+               },
+               {
+                       label:         "Missing stack name parameter",
+                       expectedError: "stack_name is missing from template_data",
+                       expectedCode:  http.StatusBadRequest,
+                       input: bytes.NewBuffer([]byte(`{
+                               "vf-module-model-customization-id": "84sdfkio938",
+                               "vf-module-model-invariant-id": "123456qwerty",
+                               "vf-module-model-version-id": "123qweasdzxc",
+                               "generic-vnf-id": "dummy-vnf-id",
+                               "vf-module-id": "dummy-vfm-id",
+                               "template_data": {
+                               },
+                               "sdnc_directives": {
+                                       "attributes": [
                                                {
                                                        "attribute_name": "k8s-rb-profile-name",
-                                                       "attribute_value": "profile1"
+                                                       "attribute_value": "dummy-profile"
                                                }
                                        ]
                                }
                        }`)),
-                       expectedCode: http.StatusBadRequest,
                },
                {
-                       label: "Missing parameter from sdnc_directives",
+                       label:         "Missing profile name directive",
+                       expectedError: "k8s-rb-profile-name is missing from directives",
+                       expectedCode:  http.StatusBadRequest,
                        input: bytes.NewBuffer([]byte(`{
                                "vf-module-model-customization-id": "84sdfkio938",
                                "vf-module-model-invariant-id": "123456qwerty",
                                "vf-module-model-version-id": "123qweasdzxc",
+                               "generic-vnf-id": "dummy-vnf-id",
+                               "vf-module-id": "dummy-vfm-id",
+                               "template_data": {
+                                       "stack_name": "dummy-stack-name"
+                               },
+                               "sdnc_directives": {
+                                       "attributes": [
+                                       ]
+                               }
+                       }`)),
+               },
+               {
+                       label:         "Missing vf-module-id parameter",
+                       expectedError: "vf-module-id is empty",
+                       expectedCode:  http.StatusBadRequest,
+                       input: bytes.NewBuffer([]byte(`{
+                               "vf-module-model-customization-id": "84sdfkio938",
+                               "vf-module-model-invariant-id": "123456qwerty",
+                               "vf-module-model-version-id": "123qweasdzxc",
+                               "generic-vnf-id": "dummy-vnf-id",
+                               "template_data": {
+                                       "stack_name": "dummy-stack-name"
+                               },
                                "sdnc_directives": {
                                        "attributes": [
                                                {
-                                                       "attribute_name": "vf_module_name",
-                                                       "attribute_value": "test-vf-module-name"
+                                                       "attribute_name": "k8s-rb-profile-name",
+                                                       "attribute_value": "dummy-profile"
                                                }
                                        ]
                                }
                        }`)),
-                       expectedCode: http.StatusBadRequest,
                },
                {
                        label: "Succesfully create an Instance",
@@ -90,15 +240,16 @@ func TestBrokerCreateHandler(t *testing.T) {
                                "vf-module-model-customization-id": "84sdfkio938",
                                "vf-module-model-invariant-id": "123456qwerty",
                                "vf-module-model-version-id": "123qweasdzxc",
+                               "generic-vnf-id": "dummy-vnf-id",
+                               "vf-module-id": "dummy-vfm-id",
+                               "template_data": {
+                                       "stack_name": "dummy-stack-name"
+                               },
                                "sdnc_directives": {
                                        "attributes": [
-                                               {
-                                                       "attribute_name": "vf_module_name",
-                                                       "attribute_value": "test-vf-module-name"
-                                               },
                                                {
                                                        "attribute_name": "k8s-rb-profile-name",
-                                                       "attribute_value": "profile1"
+                                                       "attribute_value": "dummy-profile"
                                                }
                                        ]
                                }
@@ -163,11 +314,13 @@ func TestBrokerCreateHandler(t *testing.T) {
 
                        request := httptest.NewRequest("POST", "/cloudowner/cloudregion/infra_workload", testCase.input)
                        resp := executeRequest(request, NewRouter(nil, nil, testCase.instClient, nil, nil, nil))
+                       defer resp.Body.Close()
 
                        if testCase.expectedCode != resp.StatusCode {
                                body, _ := ioutil.ReadAll(resp.Body)
                                t.Log(string(body))
-                               t.Fatalf("Request method returned: \n%v\n and it was expected: \n%v", resp.StatusCode, testCase.expectedCode)
+                               t.Fatalf("Request method returned code '%v', but '%v' was expected",
+                                       resp.StatusCode, testCase.expectedCode)
                        }
 
                        if resp.StatusCode == http.StatusCreated {
@@ -180,6 +333,16 @@ func TestBrokerCreateHandler(t *testing.T) {
                                        t.Fatalf("TestGetHandler returned:\n result=%v\n expected=%v",
                                                response, testCase.expected)
                                }
+                       } else if testCase.expectedError != "" {
+                               body, err := ioutil.ReadAll(resp.Body)
+                               if err == nil {
+                                       if !strings.Contains(string(body), testCase.expectedError) {
+                                               t.Fatalf("Request method returned body '%s', but '%s' wasn't found",
+                                                       body, testCase.expectedError)
+                                       }
+                               } else {
+                                       t.Fatalf("Request method returned malformed body")
+                               }
                        }
                })
        }
index f95c0fd..1e676c5 100644 (file)
@@ -75,6 +75,15 @@ func TestProcessValues(t *testing.T) {
                        expectedHash:  "516fab4ab7b76ba2ff35a97c2a79b74302543f532857b945f2fe25e717e755be",
                        expectedError: "",
                },
+               {
+                       label: "Process complex Pair Override",
+                       values: []string{
+                               "name={a,b,c}",
+                               "servers[0].port=80",
+                       },
+                       expectedError: "",
+                       expectedHash:  "50d9401b003f65c1ccfd1c5155106fff88c8201ab8b7d66bd6ffa4fe2883bead",
+               },
        }
 
        h := sha256.New()
@@ -96,7 +105,7 @@ func TestProcessValues(t *testing.T) {
                                gotHash := fmt.Sprintf("%x", h.Sum(nil))
                                h.Reset()
                                if gotHash != testCase.expectedHash {
-                                       t.Fatalf("Got unexpected values.yaml %s", out)
+                                       t.Fatalf("Got unexpected hash '%s' of values.yaml:\n%s", gotHash, out)
                                }
                        }
                })