diff options
Diffstat (limited to 'src')
77 files changed, 11404 insertions, 59 deletions
diff --git a/src/inventory/Makefile b/src/inventory/Makefile new file mode 100644 index 00000000..3eff12dc --- /dev/null +++ b/src/inventory/Makefile @@ -0,0 +1,39 @@ +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2020 Tech Mahindra +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +PWD := $(shell pwd) +PLATFORM := linux +BINARY := statusquey + +export GO111MODULE=on + +all: test build +deploy: test build + +build: clean test cover + CGO_ENABLED=0 GOOS=$(PLATFORM) GOARCH=amd64 + go build -a -ldflags '-extldflags "-static"' \ + -o $(PWD)/$(BINARY) controller/main.go + +deploy: build + +.PHONY: test +test: clean + @go test -v ./... + +format: + @go fmt ./... + +clean: + @rm -f $(BINARY) + +.PHONY: cover +cover: + @go test -p 2 ./... -coverprofile=coverage.out + @go tool cover -html=coverage.out -o coverage.html diff --git a/src/inventory/api/aaipullapi.go b/src/inventory/api/aaipullapi.go new file mode 100644 index 00000000..c8e4ca38 --- /dev/null +++ b/src/inventory/api/aaipullapi.go @@ -0,0 +1,77 @@ +/* +Copyright 2020 Tech Mahindra. +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 ( + "crypto/tls" + "encoding/json" + con "github.com/onap/multicloud-k8s/src/inventory/constants" + log "github.com/onap/multicloud-k8s/src/inventory/logutils" + util "github.com/onap/multicloud-k8s/src/inventory/utils" + "io/ioutil" + "net/http" + "os" +) + +func GetTenant(cloudOwner, cloudRegion string) string { + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + apiToCR := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_CREP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "?depth=all" + req, err := http.NewRequest(http.MethodGet, apiToCR, nil) + if err != nil { + log.Error("Error while constructing request for Tenant API") + return + + } + + util.SetRequestHeaders(req) + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + log.Error("Error while executing request for Tenant API") + return + } + + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + + log.Error("Can't read Tenant response") + return + + } + + var tenant con.Tenant + + json.Unmarshal([]byte(body), &tenant) + + for k, v := range tenant.Tenants { + if k == "tenant" { + for _, val := range v { + return val.TenantId + + } + } + } + + return "" + +} diff --git a/src/inventory/api/aaipullapi_test.go b/src/inventory/api/aaipullapi_test.go new file mode 100644 index 00000000..4d838e38 --- /dev/null +++ b/src/inventory/api/aaipullapi_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2020 Tech Mahindra. +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 ( + "crypto/tls" + con "github.com/onap/multicloud-k8s/src/inventory/constants" + util "github.com/onap/multicloud-k8s/src/inventory/utils" + "io/ioutil" + "net/http" + "os" + "strings" + "testing" +) + +func TestGetTenant(t *testing.T) { + + cloudOwner := "CloudOwner" + cloudRegion := "RegionOne" + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + uri := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_CREP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "?depth=all" + + req, err := http.NewRequest("GET", uri, nil) + + util.SetRequestHeaders(req) + + if err != nil { + t.Fatal(err) + } + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + t.Fatal(err) + } + + if res.StatusCode != http.StatusOK { + + t.Fail() + } + + if p, err := ioutil.ReadAll(res.Body); err != nil { + t.Fail() + } else { + if strings.Contains(string(p), "Error") { + t.Errorf("header response shouldn't return error: %s", p) + } else if !strings.Contains(string(p), `tenant`) { + t.Errorf("header response doen't match:\n%s", p) + } + } +} diff --git a/src/inventory/api/aaipushapi.go b/src/inventory/api/aaipushapi.go new file mode 100644 index 00000000..7faf498e --- /dev/null +++ b/src/inventory/api/aaipushapi.go @@ -0,0 +1,189 @@ +/* +Copyright 2020 Tech Mahindra. +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" + "crypto/tls" + "encoding/json" + con "github.com/onap/multicloud-k8s/src/inventory/constants" + log "github.com/onap/multicloud-k8s/src/inventory/logutils" + util "github.com/onap/multicloud-k8s/src/inventory/utils" + "io/ioutil" + "net/http" + "os" + "strconv" +) + +/* Pushes each pod related details as vservers inot A&AI + +{ + "vserver-id": "example20", + "vserver-name": "POD-NAME", + "vserver-name2": "Relese-name/Profile-name of the POD (Labels:release=profile-k8s)", + "prov-status": "NAMESPACEofthPOD", + "vserver-selflink": "example-vserver-selflink-val-57201", + "in-maint": true, + "is-closed-loop-disabled": true, + "l-interfaces": { + "l-interface": [{ + "interface-name": "example-interface-name-val-20080", + "is-port-mirrored": true, + "in-maint": true, + "is-ip-unnumbered": true, + "l3-interface-ipv4-address-list": [{ + "l3-interface-ipv4-address": "IP_Address", + "l3-interface-ipv4-prefix-length": "PORT" + }] + }] + } +} + +*/ +func PushVservers(podInfo con.PodInfoToAAI, cloudOwner, cloudRegion, tenantId string) string { + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + payload := "{\"vserver-name\":" + "\"" + podInfo.VserverName + "\"" + ", \"vserver-name2\":" + "\"" + podInfo.VserverName2 + "\"" + ", \"prov-status\":" + "\"" + podInfo.ProvStatus + "\"" + ",\"vserver-selflink\":" + "\"example-vserver-selflink-val-57201\", \"l-interfaces\": {\"l-interface\": [{\"interface-name\": \"example-interface-name-val-20080\",\"is-port-mirrored\": true,\"in-maint\": true,\"is-ip-unnumbered\": true,\"l3-interface-ipv4-address-list\": [{\"l3-interface-ipv4-address\":" + "\"" + podInfo.I3InterfaceIPv4Address + "\"" + ",\"l3-interface-ipv4-prefix-length\":" + "\"" + strconv.FormatInt(int64(podInfo.I3InterfaceIPvPrefixLength), 10) + "\"" + "}]}]}}" + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + url := AAI_URI + ":" + AAI_Port + con.AAI_EP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "/tenants/tenant/" + tenantId + "/vservers/vserver/" + podInfo.VserverName + + var jsonStr = []byte(payload) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr)) + + if err != nil { + log.Error("Error while constructing Vserver PUT request") + return + } + + util.SetRequestHeaders(req) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Error("Error while executing Vserver PUT api") + return + } + defer resp.Body.Close() + + return podInfo.VserverName +} + +/* This links vservers to vf-module request payload */ +func LinkVserverVFM(vnfID, vfmID, cloudOwner, cloudRegion, tenantId string, relList []con.RelationList) { + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + apiToCR := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_NEP + "/" + vnfID + "/vf-modules" + req, err := http.NewRequest(http.MethodGet, apiToCR, nil) + if err != nil { + log.Error("Error while constructing VFModules GET api request") + return + + } + + util.SetRequestHeaders(req) + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + log.Error("Error while executing VFModules GET api") + return + } + + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + + log.Error("Error while reading vfmodules API response") + return + + } + + var vfmodules con.VFModules + + json.Unmarshal([]byte(body), &vfmodules) + + vfmList := vfmodules.VFModules + + for key, vfmodule := range vfmList { + + if vfmodule.VFModuleId == vfmID { + + vfmodule.RelationshipList = map[string][]con.RelationList{"relationship": relList} + + vfmList = append(vfmList, vfmodule) + + vfmList[key] = vfmList[len(vfmList)-1] // Copy last element to index i. + vfmList = vfmList[:len(vfmList)-1] + + //update vfmodule with vserver data + + vfmPayload, err := json.Marshal(vfmodule) + + if err != nil { + + log.Error("Error while marshalling vfmodule linked vserver info response") + return + + } + + pushVFModuleToAAI(string(vfmPayload), vfmID, vnfID) + } + + } + +} + +/* Pushes vf-module enriched with vserver information */ +func pushVFModuleToAAI(vfmPayload, vfmID, vnfID string) { + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + url := AAI_URI + ":" + AAI_Port + con.AAI_NEP + vnfID + "/vfmodules/vf-module/" + vfmID + + var jsonStr = []byte(vfmPayload) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr)) + + if err != nil { + log.Error("Error while constructing a VFModule request to AAI") + return + } + + util.SetRequestHeaders(req) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + + log.Error("Error while executing PUT request of VFModule to AAI") + return + + } + + defer resp.Body.Close() + +} diff --git a/src/inventory/api/aaipushapi_test.go b/src/inventory/api/aaipushapi_test.go new file mode 100644 index 00000000..6c0dea3f --- /dev/null +++ b/src/inventory/api/aaipushapi_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2020 Tech Mahindra. +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" + "crypto/tls" + con "github.com/onap/multicloud-k8s/src/inventory/constants" + util "github.com/onap/multicloud-k8s/src/inventory/utils" + "net/http" + "os" + "testing" +) + +func TestPushVservers(t *testing.T) { + + cloudOwner := "CloudOwner" + cloudRegion := "RegionOne" + tenantId := "tenant123" + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + payload := "{\"vserver-name\":" + "\"" + "pod123" + "\"" + ", \"vserver-name2\":" + "\"" + "profile123" + "\"" + ", \"prov-status\":" + "\"" + "default" + "\"" + ",\"vserver-selflink\":" + "\"example-vserver-selflink-val-57201\", \"l-interfaces\": {\"l-interface\": [{\"interface-name\": \"example-interface-name-val-20080\",\"is-port-mirrored\": true,\"in-maint\": true,\"is-ip-unnumbered\": true,\"l3-interface-ipv4-address-list\": [{\"l3-interface-ipv4-address\":" + "\"" + "10.214.22.220" + "\"" + ",\"l3-interface-ipv4-prefix-length\":" + "\"" + "667" + "\"" + "}]}]}}" + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + url := AAI_URI + ":" + AAI_Port + con.AAI_EP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "/tenants/tenant/" + tenantId + "/vservers/vserver/" + "pod123" + + var jsonStr = []byte(payload) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr)) + + if err != nil { + + t.Error("Failed: Error consructing request") + } + + util.SetRequestHeaders(req) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + + t.Error("Failed: Error while executing request ") + } + + if resp.StatusCode != http.StatusOK { + t.Fatalf("recieved unexpeted response ") + } + +} + +func TestLinkVserverVFM(t *testing.T) { + + vnfID := "vnf123456" + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + apiToCR := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_NEP + "/" + vnfID + "/vf-modules" + req, err := http.NewRequest(http.MethodGet, apiToCR, nil) + if err != nil { + + t.Error("Failed: Error while constructing new request") + + } + + util.SetRequestHeaders(req) + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + + t.Error("Failed: Error while executing request") + } + + if res.StatusCode != http.StatusOK { + t.Fatalf("recieved unexpeted response ") + } + +} + +func TestPushVFModuleToAAI(t *testing.T) { + + vnfID := "vnf123456" + vfmID := "vfm123456" + vfmPayload := "" + + AAI_URI := os.Getenv("onap-aai") + AAI_Port := os.Getenv("aai-port") + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + url := AAI_URI + ":" + AAI_Port + con.AAI_NEP + vnfID + "/vfmodules/vf-module/" + vfmID + + var jsonStr = []byte(vfmPayload) + + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr)) + + if err != nil { + t.Error("Failed: Error while executing request") + + } + + util.SetRequestHeaders(req) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + + t.Error("Failed: Error while executing request") + } + + if resp.StatusCode != http.StatusOK { + t.Fatalf("recieved unexpeted response ") + } + +} diff --git a/src/inventory/api/k8spluginapi.go b/src/inventory/api/k8spluginapi.go new file mode 100644 index 00000000..4348d287 --- /dev/null +++ b/src/inventory/api/k8spluginapi.go @@ -0,0 +1,120 @@ +/* +Copyright 2020 Tech Mahindra. +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 ( + con "github.com/onap/multicloud-k8s/src/inventory/constants" + log "github.com/onap/multicloud-k8s/src/inventory/logutils" + utils "github.com/onap/multicloud-k8s/src/inventory/utils" + k8sint "github.com/onap/multicloud-k8s/src/k8splugin/internal/app" + k8scon "github.com/onap/multicloud-k8s/src/k8splugin/internal/connection" + + "encoding/json" + "net/http" + "os" +) + +func ListInstances() []string { + + MK8S_URI := os.Getenv("onap-multicloud-k8s") + MK8S_Port := os.Getenv("multicloud-k8s-port") + + instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_EP + req, err := http.NewRequest(http.MethodGet, instancelist, nil) + if err != nil { + + log.Error("Something went wrong while listing resources - contructing request") + return + } + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + log.Error("Something went wrong while listing resources - executing request") + return + } + + defer res.Body.Close() + + decoder := json.NewDecoder(res.Body) + var rlist []k8sint.InstanceMiniResponse + err = decoder.Decode(&rlist) + + resourceList := utils.ParseListInstanceResponse(rlist) + + return resourceList + +} + +func GetConnection(cregion string) k8scon.Connection { + + MK8S_URI := os.Getenv("onap-multicloud-k8s") + MK8S_Port := os.Getenv("multicloud-k8s-port") + + instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_CEP + cregion + req, err := http.NewRequest(http.MethodGet, instancelist, nil) + if err != nil { + + log.Error("Something went wrong while getting Connection resource - contructing request") + return + } + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + log.Error("Something went wrong while getting Connection resource - executing request") + return + } + + defer res.Body.Close() + + decoder := json.NewDecoder(res.Body) + var connection k8scon.Connection + err = decoder.Decode(&connection) + + return connection + +} + +func CheckStatusForEachInstance(instanceID string) k8sint.InstanceStatus { + + MK8S_URI := os.Getenv("onap-multicloud-k8s") + MK8S_Port := os.Getenv("multicloud-k8s-port") + + instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_EP + instanceID + "/status" + + req, err := http.NewRequest(http.MethodGet, instancelist, nil) + if err != nil { + log.Error("Error while checking instance status - building http request") + return + } + + client := http.DefaultClient + resp, err := client.Do(req) + if err != nil { + + log.Error("Error while checking instance status - making rest request") + return + } + + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + var instStatus k8sint.InstanceStatus + err = decoder.Decode(&instStatus) + + return instStatus +} diff --git a/src/inventory/api/k8spluginapi_test.go b/src/inventory/api/k8spluginapi_test.go new file mode 100644 index 00000000..6eb16486 --- /dev/null +++ b/src/inventory/api/k8spluginapi_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2020 Tech Mahindra. +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 ( + "crypto/tls" + con "github.com/onap/multicloud-k8s/src/inventory/constants" + util "github.com/onap/multicloud-k8s/src/inventory/utils" + "io/ioutil" + "net/http" + "os" + "strings" + "testing" +) + +func TestListInstances(t *testing.T) { + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + MK8S_URI := os.Getenv("onap-multicloud-k8s") + MK8S_Port := os.Getenv("multicloud-k8s-port") + + instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_EP + req, err := http.NewRequest(http.MethodGet, instancelist, nil) + + util.SetRequestHeaders(req) + + if err != nil { + t.Fatal(err) + } + + client := http.DefaultClient + res, err := client.Do(req) + + if err != nil { + t.Fatal(err) + } + + if res.StatusCode != http.StatusOK { + + t.Fail() + } + + if p, err := ioutil.ReadAll(res.Body); err != nil { + t.Fail() + } else { + if strings.Contains(string(p), "Error") { + t.Errorf("header response shouldn't return error: %s", p) + } else if !strings.Contains(string(p), `cloud-region`) { + t.Errorf("header response doen't match:\n%s", p) + } + } +} diff --git a/src/inventory/constants/const.go b/src/inventory/constants/const.go new file mode 100644 index 00000000..a1ba2334 --- /dev/null +++ b/src/inventory/constants/const.go @@ -0,0 +1,94 @@ +/* +Copyright 2020 Tech Mahindra. +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 constants + +import ( + +//corev1 "k8s.io/api/core/v1" + +) + +const ( + XFromAppId = "SO" + ContentType = "application/json" + Accept = "application/json" + XTransactionId = "get_aai_subscr" +) + +const ( + AAI_EP = "/aai/v14" + AAI_CREP = "/cloud-infrastructure/cloud-regions/" + AAI_NEP = "/network/generic-vnfs/generic-vnf/" +) + +const ( + MK8S_EP = "/api/multicloud-k8s/v1/v1/instance/" + MK8S_CEP = "/connectivity-info" +) + +type PodInfoToAAI struct { + VserverName string + VserverName2 string + ProvStatus string + I3InterfaceIPv4Address string + I3InterfaceIPvPrefixLength int32 + VnfId string + VfmId string + CloudRegion string +} + +type RData struct { + RelationshipKey string `json:"relationship-key"` + RelationshipValue string `json:"relationship-value"` +} + +type RelationList struct { + RelatedTo string `json:"related-to"` + RelatedLink string `json:"related-link"` + RelationshipData []RData `json:"relationship-data"` + RelatedToProperty []Property `json:"related-to-property"` +} + +type TenantInfo struct { + TenantId string `json:"tenant-id"` + TenantName string `json:"tenant-name"` +} + +type Tenant struct { + Tenants map[string][]TenantInfo `json:"tenants"` +} + +type Property struct { + PropertyKey string `json:"property-key"` + PropertyValue string `json:"property-value"` +} + +type VFModule struct { + VFModuleId string `json:"vf-module-id"` + VFModuleName string `json:"vf-module-name"` + HeatStackId string `json:"heat-stack-id"` + OrchestrationStatus string `json:"orchestration-status"` + ResourceVersion string `json:"resource-version"` + AutomatedAssignment string `json:"automated-assignment"` + IsBaseVfModule string `json:"is-base-vf-module"` + RelationshipList map[string][]RelationList `json:"relationship-list"` + ModelInvariantId string `json:"model-invariant-id"` + ModelVersionId string `json:"model-version-id"` + ModelCustomizationId string `json:"model-customization-id"` + ModuleIndex string `json:"module-index"` +} + +type VFModules struct { + VFModules []VFModule `json:"vf-module"` +} diff --git a/src/inventory/controller/main.go b/src/inventory/controller/main.go new file mode 100644 index 00000000..5e28282d --- /dev/null +++ b/src/inventory/controller/main.go @@ -0,0 +1,83 @@ +/* +Copyright 2020 Tech Mahindra. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + executor "github.com/onap/multicloud-k8s/src/inventory/api" + con "github.com/onap/multicloud-k8s/src/inventory/constants" + k8splugin "github.com/onap/multicloud-k8s/src/k8splugin/internal/app" + utils "github.com/onap/multicloud-k8s/src/inventory/utils" + "os" + "os/signal" + "time" +) + +/* Root function which periodically polls status api for all the instances in the k8splugin and update the status information accordingly to AAI */ +func QueryAAI() { + + for { + instanceList := executor.ListInstances() + statusList := CheckInstanceStatus(instanceList) + podList := utils.ParseStatusInstanceResponse(statusList) + PushPodInfoToAAI(podList) + time.Sleep(360000 * time.Second) + } + +} + +func CheckInstanceStatus(instanceList []string) []k8splugin.InstanceStatus { + + var instStatusList []k8splugin.InstanceStatus + + for _, instance := range instanceList { + + instanceStatus := executor.CheckStatusForEachInstance(string(instance)) + + instStatusList = append(instStatusList, instanceStatus) + + } + + return instStatusList +} + +func PushPodInfoToAAI(podList []con.PodInfoToAAI) { + + var relList []con.RelationList + + for _, pod := range podList { + + connection := executor.GetConnection(pod.CloudRegion) + + tenantId := executor.GetTenant(connection.CloudOwner, pod.CloudRegion) + + vserverID := executor.PushVservers(pod, connection.CloudOwner, pod.CloudRegion, tenantId) + + rl := utils.BuildRelationshipDataForVFModule(pod.VserverName, vserverID, connection.CloudOwner, pod.CloudRegion, tenantId) + relList = append(relList, rl) + + executor.LinkVserverVFM(pod.VnfId, pod.VfmId, connection.CloudOwner, pod.CloudRegion, tenantId, relList) + } + +} + +func main() { + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + //go QueryAAI() + + <-c + +} diff --git a/src/inventory/go.mod b/src/inventory/go.mod new file mode 100644 index 00000000..e7d94194 --- /dev/null +++ b/src/inventory/go.mod @@ -0,0 +1,8 @@ +module github.com/onap/multicloud-k8s/src/inventory + +go 1.12 + +require ( + github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20200303230813-37aed9b7a0db // indirect + k8s.io/api v0.17.2 +) diff --git a/src/inventory/go.sum b/src/inventory/go.sum new file mode 100644 index 00000000..3a648d49 --- /dev/null +++ b/src/inventory/go.sum @@ -0,0 +1,458 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.17.1+incompatible h1:PChbxFGKTWsg9IWh+pSZRCSj3zQkVpL6Hd9uWsFwxtc= +github.com/Masterminds/sprig v2.17.1+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/VamshiKrishnaNemalikonda/GO v0.0.0-20191112095043-d6b01bbf585f h1:I3PTea8BCbUFS3YCRYJwHN9ppi5CuN+pmH10NtNRt+A= +github.com/aokoli/goutils v1.1.0 h1:jy4ghdcYvs5EIoGssZNslIASX5m+KNMfyyKvRQ0TEVE= +github.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 h1:HD4PLRzjuCVW79mQ0/pdsalOLHJ+FaEoqJLxfltpb2U= +github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.12+incompatible h1:pAWNwdf7QiT1zfaWyqCtNZQWCLByQyA3JrSQyuYAqnQ= +github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU= +github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190912223608-ad718029b705 h1:up4REDeXtcm77SlkowEGUuakgjpdNR2N9TkGTZSL4rM= +github.com/docker/docker v0.7.3-0.20190912223608-ad718029b705/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/engine v0.0.0-20190620014054-c513a4c6c298 h1:dDGt5n84DvY05kaJT26cw1TDxNW1NymRZ13j0KeEQaw= +github.com/docker/engine v0.0.0-20190620014054-c513a4c6c298/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff h1:kOkM9whyQYodu09SJ6W3NCsHG7crFaJILQ22Gozp3lg= +github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM= +github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.11.1/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul v1.4.0 h1:PQTW4xCuAExEiSbhrsFsikzbW5gVBoi74BjUvYFyKHw= +github.com/hashicorp/consul v1.4.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:VBj0QYQ0u2MCJzBfeYXGexnAl17GsH1yidnoxCqqD9E= +github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.1 h1:mYs6SMzu72+90OcPa5wr3nfznA4Dw9UyR791ZFNOIf4= +github.com/hashicorp/serf v0.8.1/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onap/multicloud-k8s v0.0.0-20200123181220-fc960539db54 h1:0iPySKR2wPGiyvi0KmtjNmWcBoM8oUUVc3Q4QkYbkp0= +github.com/onap/multicloud-k8s v0.0.0-20200210192739-c06be6458e99 h1:ZVCLL4/4dEbq/G9XGWhl3y8b1jgTB03ByTI2AiVMgUk= +github.com/onap/multicloud-k8s v0.0.0-20200303230813-37aed9b7a0db h1:H+IVbOqoE4rk2qcSwOrR335VejyBhkLK75V6UL2uoJ4= +github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20200303230813-37aed9b7a0db h1:RevNsc1iFXT2DGBdYViLrJJ0HXIoAxdvj6QopQOKU5I= +github.com/onap/multicloud-k8s/src/k8splugin v0.0.0-20200303230813-37aed9b7a0db/go.mod h1:EnQd/vQGZR1/55IihaHxiux4ZUig/zfXZux7bfmU0S8= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rubenv/sql-migrate v0.0.0-20190902133344-8926f37f0bc1 h1:G7j/gxkXAL80NMLOWi6EEctDET1Iuxl3sBMJXDnu2z0= +github.com/rubenv/sql-migrate v0.0.0-20190902133344-8926f37f0bc1/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/technosophos/moniker v0.0.0-20180509230615-a5dbd03a2245 h1:DNVk+NIkGS0RbLkjQOLCJb/759yfCysThkMbl7EXxyY= +github.com/technosophos/moniker v0.0.0-20180509230615-a5dbd03a2245/go.mod h1:O1c8HleITsZqzNZDjSNzirUGsMT0oGu9LhHKoJrqO+A= +github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v3.3.12+incompatible h1:V6PRYRGpU4k5EajJaaj/GL3hqIdzyPnBU8aPUp+35yw= +go.etcd.io/etcd v3.3.12+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= +go.mongodb.org/mongo-driver v1.0.0 h1:KxPRDyfB2xXnDE2My8acoOWBQkfv3tz0SaWTRZjJR0c= +go.mongodb.org/mongo-driver v1.0.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190831074750-7364b6bdad65/go.mod h1:u09ZxrpPFcoUNEQM2GsqT/KpglKAtXdEcK+tSMilQ3Q= +k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= +k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8 h1:q1Qvjzs/iEdXF6A1a8H3AKVFDzJNcJn3nXMs6R6qFtA= +k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE= +k8s.io/apimachinery v0.0.0-20190831074630-461753078381/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= +k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= +k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c h1:k7ALUVzrOEgz4hOF+pr4pePn7TqZ9lB/8Z8ndMSsWSU= +k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w= +k8s.io/cli-runtime v0.0.0-20190913085402-777c64e2902f h1:gQH9KuiqXEhXWEHnov9bS/ysYPSIYNT1P3BWC9HGI7M= +k8s.io/cli-runtime v0.0.0-20190913085402-777c64e2902f/go.mod h1:TtjkdmxYMLASzYbE8E7AUr/ZrXMcmXLnDLRY4sVWspw= +k8s.io/client-go v0.0.0-20190831074946-3fe2abece89e/go.mod h1:hAiMqq+tCk9hxFvWr2DoRiVyCYEGpni4eOcGCQLOEfM= +k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible h1:U5Bt+dab9K8qaUmXINrkXO135kA11/i5Kg1RUydgaMQ= +k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/cloud-provider v0.0.0-20190409023720-1bc0c81fa51d h1:ad7UpNUGRx6FbYoK4+xIYyeS2CUShjNKY45YN1ipjLI= +k8s.io/cloud-provider v0.0.0-20190409023720-1bc0c81fa51d/go.mod h1:LlIffnLBu+GG7d4ppPzC8UnA1Ex8S+ntmSRVsnr7Xy4= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/helm v2.14.3+incompatible h1:uzotTcZXa/b2SWVoUzM1xiCXVjI38TuxMujS/1s+3Gw= +k8s.io/helm v2.14.3+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kubernetes v1.14.1 h1:I9F52h5sqVxBmoSsBlNQ0YygNcukDilkpGxUbJRoBoY= +k8s.io/kubernetes v1.14.1/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20190907131718-3d4f5b7dea0b h1:eMM0sTvh3KBVGwJfuNcU86P38TJhlVMAICbFPDG3t0M= +k8s.io/utils v0.0.0-20190907131718-3d4f5b7dea0b/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= +sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= +vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/src/inventory/utils/util.go b/src/inventory/utils/util.go new file mode 100644 index 00000000..8d204f92 --- /dev/null +++ b/src/inventory/utils/util.go @@ -0,0 +1,119 @@ +/* +Copyright 2020 Tech Mahindra. +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 ( + con "github.com/onap/multicloud-k8s/src/inventory/constants" + k8splugin "github.com/onap/multicloud-k8s/src/k8splugin/internal/app" + "net/http" + "os" + "reflect" +) + +/* Building relationship json to attach vserver details to vf-module*/ +func BuildRelationshipDataForVFModule(vserverName, vserverID, cloudOwner, cloudRegion, tenantId string) con.RelationList { + + rl := con.RelationList{"vserver", "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/" + cloudOwner + "/" + cloudRegion + "/tenants/tenant/" + tenantId + "/vservers/vserver/" + vserverID, []con.RData{con.RData{"cloud-region.cloud-owner", cloudOwner}, + con.RData{"cloud-region.cloud-region-id", cloudRegion}, + con.RData{"tenant.tenant-id", tenantId}, + con.RData{"vserver.vserver-id", vserverID}}, + []con.Property{con.Property{"vserver.vserver-name", vserverName}}} + + return rl + +} + +func ParseListInstanceResponse(rlist []k8splugin.InstanceMiniResponse) []string { + + var resourceIdList []string + + //assume there is only one resource created + for _, result := range rlist { + + resourceIdList = append(resourceIdList, result.ID) + } + + return resourceIdList +} + +/* Parse status api response to pull required information like Pod name, Profile name, namespace, ip details, vnf-id and vf-module-id*/ +func ParseStatusInstanceResponse(instanceStatusses []k8splugin.InstanceStatus) []con.PodInfoToAAI { + + var infoToAAI []con.PodInfoToAAI + + for _, instanceStatus := range instanceStatusses { + + var podInfo con.PodInfoToAAI + + sa := reflect.ValueOf(&instanceStatus).Elem() + typeOf := sa.Type() + for i := 0; i < sa.NumField(); i++ { + f := sa.Field(i) + if typeOf.Field(i).Name == "Request" { + request := f.Interface() + if ireq, ok := request.(k8splugin.InstanceRequest); ok { + podInfo.VserverName2 = ireq.ProfileName + podInfo.CloudRegion = ireq.CloudRegion + + for key, value := range ireq.Labels { + if key == "generic-vnf-id" { + + podInfo.VnfId = value + + } + if key == "vfmodule-id" { + + podInfo.VfmId = value + + } + } + + } else { + //fmt.Printf("it's not a InstanceRequest \n") + } + } + + if typeOf.Field(i).Name == "PodStatuses" { + ready := f.Interface() + if pss, ok := ready.([]con.PodStatus); ok { + for _, ps := range pss { + podInfo.VserverName = ps.Name + podInfo.ProvStatus = ps.Namespace + } + + } else { + //fmt.Printf("it's not a InstanceRequest \n") + } + } + } + + infoToAAI = append(infoToAAI, podInfo) + + } + + return infoToAAI + +} + +/* this sets http headers to request object*/ +func SetRequestHeaders(req *http.Request) { + authorization := os.Getenv("authorization") + + req.Header.Set("X-FromAppId", con.XFromAppId) + req.Header.Set("Content-Type", con.ContentType) + req.Header.Set("Accept", con.Accept) + req.Header.Set("X-TransactionId", con.XTransactionId) + req.Header.Set("Authorization", authorization) + +} diff --git a/src/inventory/utils/util_test.go b/src/inventory/utils/util_test.go new file mode 100644 index 00000000..4dd0c133 --- /dev/null +++ b/src/inventory/utils/util_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2020 Tech Mahindra. +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 ( + con "github.com/onap/multicloud-k8s/src/inventory/constants" + k8splugin "github.com/onap/multicloud-k8s/src/k8splugin/internal/app" + "testing" +) + +func TestBuildRelationshipDataForVFModule(t *testing.T) { + + relList := BuildRelationshipDataForVFModule("vs_name", "vs1234", "CO", "CR", "tenant1234") + + if relList.RelatedTo != "vserver" { + t.Error("Failed") + } + + if (relList.RelatedLink) != "/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/CO/CR/tenants/tenant/tenant1234/vservers/vserver/vs1234" { + t.Error("Failed") + } + + rdadaList := relList.RelationshipData + + for _, rdata := range rdadaList { + + if rdata.RelationshipKey == "cloud-region.cloud-region-id" { + + if rdata.RelationshipValue != "CR" { + + t.Error("Failed") + + } + } + + if rdata.RelationshipKey == "tenant.tenant-id" { + + if rdata.RelationshipValue != "tenant1234" { + + t.Error("Failed") + + } + } + + if rdata.RelationshipKey == "vserver.vserver-id" { + + if rdata.RelationshipValue != "vs1234" { + + t.Error("Failed") + + } + } + + if rdata.RelationshipKey == "cloud-region.cloud-owner" { + + if rdata.RelationshipValue != "CO" { + + t.Error("Failed") + + } + } + + } + + propertyList := relList.RelatedToProperty + + for _, property := range propertyList { + + if property.PropertyKey == "vserver.vserver-name" { + + if property.PropertyValue != "vs_name" { + + t.Error("Failed") + + } + } + + } + +} + +func TestParseStatusInstanceResponse(t *testing.T) { + + var resourceIdList []k8splugin.InstanceStatus + + instanceRequest := k8splugin.InstanceRequest{"rb_name", "rb_version", "profile123456", "c_region", map[string]string{"generic-vnf-id": "123456789", "vf-module-id": "987654321"}} + instanceStatus := k8splugin.InstanceStatus{instanceRequest, true, 12, []con.PodStatus{con.PodStatus{"pod123", "onap", true, []string{"10.211.1.100", "10.211.1.101"}}, con.PodStatus{"pod456", "default", true, []string{"10.211.1.200", "10.211.1.201"}}}} + + resourceIdList = append(resourceIdList, instanceStatus) + + podInfoToAAI := ParseStatusInstanceResponse(resourceIdList) + + for _, podInfo := range podInfoToAAI { + + if podInfo.VserverName == "pod123" { + + t.Error("Failed") + + } + + if podInfo.VserverName2 == "default" { + + t.Error("Failed") + + } + + if podInfo.ProvStatus == "profile123456" { + + t.Error("Failed") + + } + + } + +} diff --git a/src/k8splugin/internal/app/instance.go b/src/k8splugin/internal/app/instance.go index 5d8b2100..d6eb91b4 100644 --- a/src/k8splugin/internal/app/instance.go +++ b/src/k8splugin/internal/app/instance.go @@ -120,7 +120,7 @@ func NewInstanceClient() *InstanceClient { } } -// Create an entry for the resource bundle profile in the database +// Create an instance of rb on the cluster in the database func (v *InstanceClient) Create(i InstanceRequest) (InstanceResponse, error) { // Name is required diff --git a/src/k8splugin/internal/rb/archive.go b/src/k8splugin/internal/rb/archive.go index c0753134..267c7cd2 100644 --- a/src/k8splugin/internal/rb/archive.go +++ b/src/k8splugin/internal/rb/archive.go @@ -115,6 +115,10 @@ func ExtractTarBall(r io.Reader) (string, error) { } } case tar.TypeReg: + if target == outDir { // Handle '.' substituted to '' entry + continue + } + err = utils.EnsureDirectory(target) if err != nil { return "", pkgerrors.Wrap(err, "Creating Directory") diff --git a/src/k8splugin/internal/rb/archive_test.go b/src/k8splugin/internal/rb/archive_test.go index 5fa66e79..4c09069a 100644 --- a/src/k8splugin/internal/rb/archive_test.go +++ b/src/k8splugin/internal/rb/archive_test.go @@ -19,6 +19,7 @@ package rb import ( "bytes" "io/ioutil" + "os" "path/filepath" "strings" "testing" @@ -102,6 +103,7 @@ func TestExtractTarBall(t *testing.T) { } path, err := ExtractTarBall(bytes.NewBuffer(content)) + defer os.RemoveAll(path) if err != nil { t.Errorf("Error reading valid tar.gz file %s", err.Error()) } @@ -120,7 +122,8 @@ func TestExtractTarBall(t *testing.T) { 0x00, 0xff, 0xf2, 0x48, 0xcd, } - _, err := ExtractTarBall(bytes.NewBuffer(content)) + path, err := ExtractTarBall(bytes.NewBuffer(content)) + defer os.RemoveAll(path) if err == nil { t.Errorf("Error should NOT be nil") } @@ -128,9 +131,46 @@ func TestExtractTarBall(t *testing.T) { t.Run("Empty tar.gz", func(t *testing.T) { content := []byte{} - err := isTarGz(bytes.NewBuffer(content)) + path, err := ExtractTarBall(bytes.NewBuffer(content)) + defer os.RemoveAll(path) if err == nil { t.Errorf("Error should NOT be nil") } }) + + t.Run("Specific tar.gz with '.' entry", func(t *testing.T) { + content := []byte{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0xed, 0x94, 0xcd, 0x0a, 0xc3, 0x20, + 0x10, 0x84, 0x3d, 0xfb, 0x14, 0x4b, 0xee, 0x29, + 0x6b, 0x4c, 0x14, 0x7c, 0x99, 0x22, 0xd4, 0x42, + 0x20, 0x3f, 0x25, 0x9a, 0x40, 0xde, 0x3e, 0x92, + 0x9c, 0x1a, 0x0a, 0x39, 0x34, 0xb1, 0x87, 0xee, + 0x77, 0x51, 0xd6, 0xc3, 0x8c, 0x8c, 0x23, 0x63, + 0xd7, 0x83, 0x02, 0x51, 0x95, 0x25, 0xe0, 0xc6, + 0x7e, 0x5d, 0xf7, 0x42, 0x2a, 0x89, 0x65, 0xa5, + 0xb1, 0x2a, 0xe2, 0x5c, 0x2b, 0x2c, 0x18, 0x60, + 0x02, 0x6f, 0x6c, 0xf4, 0xc1, 0x0e, 0x0c, 0xbf, + 0xd6, 0xda, 0x5f, 0xee, 0x04, 0x6b, 0x29, 0x68, + 0x6d, 0x57, 0x3f, 0x9d, 0x0f, 0xb7, 0xd9, 0xb6, + 0xcd, 0x45, 0x1a, 0x87, 0xf9, 0x2b, 0xfd, 0x9e, + 0xbf, 0x28, 0x24, 0x6a, 0xca, 0x3f, 0x05, 0x79, + 0x9e, 0xf3, 0xc9, 0x0d, 0xbe, 0xee, 0x3b, 0x03, + 0x93, 0xe0, 0x61, 0x7e, 0x39, 0xc3, 0x01, 0x26, + 0xdb, 0x8c, 0xce, 0x1b, 0xc8, 0xfa, 0x78, 0x3a, + 0xd4, 0x0f, 0x77, 0xdf, 0x26, 0xeb, 0x3b, 0xc9, + 0xf8, 0xaf, 0x6d, 0x13, 0x27, 0xf1, 0x29, 0xde, + 0xb3, 0x35, 0x0e, 0xfb, 0x2f, 0xd4, 0xae, 0xff, + 0x52, 0x69, 0x41, 0xfd, 0x4f, 0x41, 0x88, 0x7f, + 0xff, 0x96, 0xbd, 0x01, 0x4d, 0xb5, 0x26, 0x08, + 0x82, 0xf8, 0x1b, 0x16, 0xb6, 0x17, 0x45, 0x61, + 0x00, 0x0e, 0x00, 0x00, + } + + path, err := ExtractTarBall(bytes.NewBuffer(content)) + defer os.RemoveAll(path) + if err != nil { + t.Errorf("This tar.gz should be handled correctly, but '%s' occured", err) + } + }) } diff --git a/src/ncm/Makefile b/src/ncm/Makefile new file mode 100644 index 00000000..fbdd53f7 --- /dev/null +++ b/src/ncm/Makefile @@ -0,0 +1,36 @@ +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2020 Intel Corporation +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +export GO111MODULE=on + +all: clean + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 + @go build -tags netgo -o ./ncm ./cmd/main.go + +# The following is done this way as each patch on CI runs build and each merge runs deploy. So for build we don't need to build binary and hence +# no need to create a static binary with additional flags. However, for generating binary, additional build flags are necessary. This if used with +# mock plugin errors out for unit tests. So the seperation avoids the error. + +build: clean test cover +deploy: build + +.PHONY: test +test: clean + @go test -race ./... + +format: + @go fmt ./... + +clean: + @rm -f ncm coverage.html coverage.out + +.PHONY: cover +cover: + @go test -race ./... -coverprofile=coverage.out + @go tool cover -html=coverage.out -o coverage.html diff --git a/src/ncm/api/api.go b/src/ncm/api/api.go new file mode 100644 index 00000000..34b46c67 --- /dev/null +++ b/src/ncm/api/api.go @@ -0,0 +1,107 @@ +/* + * 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 ( + "fmt" + "reflect" + + "github.com/gorilla/mux" + moduleLib "github.com/onap/multicloud-k8s/src/ncm/pkg/module" +) + +var moduleClient *moduleLib.Client + +// For the given client and testClient, if the testClient is not null and +// implements the client manager interface corresponding to client, then +// return the testClient, otherwise return the client. +func setClient(client, testClient interface{}) interface{} { + switch cl := client.(type) { + case *moduleLib.ClusterClient: + if testClient != nil && reflect.TypeOf(testClient).Implements(reflect.TypeOf((*moduleLib.ClusterManager)(nil)).Elem()) { + c, ok := testClient.(moduleLib.ClusterManager) + if ok { + return c + } + } + case *moduleLib.NetworkClient: + if testClient != nil && reflect.TypeOf(testClient).Implements(reflect.TypeOf((*moduleLib.NetworkManager)(nil)).Elem()) { + c, ok := testClient.(moduleLib.NetworkManager) + if ok { + return c + } + } + case *moduleLib.ProviderNetClient: + if testClient != nil && reflect.TypeOf(testClient).Implements(reflect.TypeOf((*moduleLib.ProviderNetManager)(nil)).Elem()) { + c, ok := testClient.(moduleLib.ProviderNetManager) + if ok { + return c + } + } + default: + fmt.Printf("unknown type %T\n", cl) + } + return client +} + +// NewRouter creates a router that registers the various urls that are supported +// testClient parameter allows unit testing for a given client +func NewRouter(testClient interface{}) *mux.Router { + + moduleClient = moduleLib.NewClient() + + router := mux.NewRouter().PathPrefix("/v2").Subrouter() + + clusterHandler := clusterHandler{ + client: setClient(moduleClient.Cluster, testClient).(moduleLib.ClusterManager), + } + router.HandleFunc("/cluster-providers", clusterHandler.createClusterProviderHandler).Methods("POST") + router.HandleFunc("/cluster-providers", clusterHandler.getClusterProviderHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{name}", clusterHandler.getClusterProviderHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{name}", clusterHandler.deleteClusterProviderHandler).Methods("DELETE") + router.HandleFunc("/cluster-providers/{provider-name}/clusters", clusterHandler.createClusterHandler).Methods("POST") + router.HandleFunc("/cluster-providers/{provider-name}/clusters", clusterHandler.getClusterHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{name}", clusterHandler.getClusterHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{name}", clusterHandler.deleteClusterHandler).Methods("DELETE") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/labels", clusterHandler.createClusterLabelHandler).Methods("POST") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/labels", clusterHandler.getClusterLabelHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/labels/{label}", clusterHandler.getClusterLabelHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/labels/{label}", clusterHandler.deleteClusterLabelHandler).Methods("DELETE") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/kv-pairs", clusterHandler.createClusterKvPairsHandler).Methods("POST") + 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") + + networkHandler := networkHandler{ + client: setClient(moduleClient.Network, testClient).(moduleLib.NetworkManager), + } + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/networks", networkHandler.createNetworkHandler).Methods("POST") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/networks", networkHandler.getNetworkHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/networks/{name}", networkHandler.putNetworkHandler).Methods("PUT") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/networks/{name}", networkHandler.getNetworkHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/networks/{name}", networkHandler.deleteNetworkHandler).Methods("DELETE") + + providernetHandler := providernetHandler{ + client: setClient(moduleClient.ProviderNet, testClient).(moduleLib.ProviderNetManager), + } + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/provider-networks", providernetHandler.createProviderNetHandler).Methods("POST") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/provider-networks", providernetHandler.getProviderNetHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/provider-networks/{name}", providernetHandler.putProviderNetHandler).Methods("PUT") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/provider-networks/{name}", providernetHandler.getProviderNetHandler).Methods("GET") + router.HandleFunc("/cluster-providers/{provider-name}/clusters/{cluster-name}/provider-networks/{name}", providernetHandler.deleteProviderNetHandler).Methods("DELETE") + + return router +} diff --git a/src/ncm/api/clusterhandler.go b/src/ncm/api/clusterhandler.go new file mode 100644 index 00000000..cb147a8a --- /dev/null +++ b/src/ncm/api/clusterhandler.go @@ -0,0 +1,468 @@ +/* + * 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/ncm/pkg/module" + + "github.com/gorilla/mux" +) + +// Used to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type clusterHandler struct { + // Interface that implements Cluster operations + // We will set this variable with a mock interface for testing + client moduleLib.ClusterManager +} + +// Create handles creation of the ClusterProvider entry in the database +func (h clusterHandler) createClusterProviderHandler(w http.ResponseWriter, r *http.Request) { + var p moduleLib.ClusterProvider + + err := json.NewDecoder(r.Body).Decode(&p) + + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateClusterProvider(p) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular ClusterProvider Name +// Returns a ClusterProvider +func (h clusterHandler) getClusterProviderHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + var ret interface{} + var err error + + if len(name) == 0 { + ret, err = h.client.GetClusterProviders() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + ret, err = h.client.GetClusterProvider(name) + 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(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular ClusterProvider Name +func (h clusterHandler) deleteClusterProviderHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + + err := h.client.DeleteClusterProvider(name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// Create handles creation of the Cluster entry in the database +func (h clusterHandler) createClusterHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + var p moduleLib.Cluster + var q moduleLib.ClusterContent + + // 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(&p) + 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 + } + + q.Kubeconfig = base64.StdEncoding.EncodeToString(content) + + // Name is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateCluster(provider, p, q) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular Cluster Name +// Returns a Cluster +func (h clusterHandler) getClusterHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + name := vars["name"] + + // handle the get all clusters case - return a list of only the json parts + if len(name) == 0 { + var retList []moduleLib.Cluster + + ret, err := h.client.GetClusters(provider) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, cl := range ret { + retList = append(retList, moduleLib.Cluster{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 + } + + accepted, _, err := mime.ParseMediaType(r.Header.Get("Accept")) + if err != nil { + http.Error(w, err.Error(), http.StatusNotAcceptable) + return + } + + retCluster, err := h.client.GetCluster(provider, name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + retKubeconfig, err := h.client.GetClusterContent(provider, 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(retCluster); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pw, err = mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/octet-stream"}, "Content-Disposition": {"form-data; name=file; filename=kubeconfig"}}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + kcBytes, err := base64.StdEncoding.DecodeString(retKubeconfig.Kubeconfig) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = pw.Write(kcBytes) + 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(retCluster) + 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) + kcBytes, err := base64.StdEncoding.DecodeString(retKubeconfig.Kubeconfig) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(kcBytes) + 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 + } +} + +// Delete handles DELETE operations on a particular Cluster Name +func (h clusterHandler) deleteClusterHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + name := vars["name"] + + err := h.client.DeleteCluster(provider, name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// Create handles creation of the ClusterLabel entry in the database +func (h clusterHandler) createClusterLabelHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + cluster := vars["cluster-name"] + var p moduleLib.ClusterLabel + + err := json.NewDecoder(r.Body).Decode(&p) + + // LabelName is required. + if p.LabelName == "" { + http.Error(w, "Missing label name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateClusterLabel(provider, cluster, p) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular Cluster Label +// Returns a ClusterLabel +func (h clusterHandler) getClusterLabelHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + cluster := vars["cluster-name"] + label := vars["label"] + + var ret interface{} + var err error + + if len(label) == 0 { + ret, err = h.client.GetClusterLabels(provider, cluster) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + ret, err = h.client.GetClusterLabel(provider, cluster, label) + 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(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular ClusterLabel Name +func (h clusterHandler) deleteClusterLabelHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + cluster := vars["cluster-name"] + label := vars["label"] + + err := h.client.DeleteClusterLabel(provider, cluster, label) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// Create handles creation of the ClusterKvPairs entry in the database +func (h clusterHandler) createClusterKvPairsHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + cluster := vars["cluster-name"] + var p moduleLib.ClusterKvPairs + + err := json.NewDecoder(r.Body).Decode(&p) + + // KvPairsName is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing Key Value pair name in POST request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateClusterKvPairs(provider, cluster, p) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular Cluster Key Value Pair +// Returns a ClusterKvPairs +func (h clusterHandler) getClusterKvPairsHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + cluster := vars["cluster-name"] + kvpair := vars["kvpair"] + + var ret interface{} + var err error + + if len(kvpair) == 0 { + ret, err = h.client.GetAllClusterKvPairs(provider, cluster) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + ret, err = h.client.GetClusterKvPairs(provider, cluster, kvpair) + 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(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular Cluster Name +func (h clusterHandler) deleteClusterKvPairsHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + provider := vars["provider-name"] + cluster := vars["cluster-name"] + kvpair := vars["kvpair"] + + err := h.client.DeleteClusterKvPairs(provider, cluster, kvpair) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/ncm/api/clusterhandler_test.go b/src/ncm/api/clusterhandler_test.go new file mode 100644 index 00000000..af5bd160 --- /dev/null +++ b/src/ncm/api/clusterhandler_test.go @@ -0,0 +1,1412 @@ +/* + * 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" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/textproto" + "reflect" + "testing" + + moduleLib "github.com/onap/multicloud-k8s/src/ncm/pkg/module" + + pkgerrors "github.com/pkg/errors" +) + +//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 mockClusterManager struct { + // Items and err will be used to customize each test + // via a localized instantiation of mockClusterManager + ClusterProviderItems []moduleLib.ClusterProvider + ClusterItems []moduleLib.Cluster + ClusterContentItems []moduleLib.ClusterContent + ClusterLabelItems []moduleLib.ClusterLabel + ClusterKvPairsItems []moduleLib.ClusterKvPairs + Err error +} + +func (m *mockClusterManager) CreateClusterProvider(inp moduleLib.ClusterProvider) (moduleLib.ClusterProvider, error) { + if m.Err != nil { + return moduleLib.ClusterProvider{}, m.Err + } + + return m.ClusterProviderItems[0], nil +} + +func (m *mockClusterManager) GetClusterProvider(name string) (moduleLib.ClusterProvider, error) { + if m.Err != nil { + return moduleLib.ClusterProvider{}, m.Err + } + + return m.ClusterProviderItems[0], nil +} + +func (m *mockClusterManager) GetClusterProviders() ([]moduleLib.ClusterProvider, error) { + if m.Err != nil { + return []moduleLib.ClusterProvider{}, m.Err + } + + return m.ClusterProviderItems, nil +} + +func (m *mockClusterManager) DeleteClusterProvider(name string) error { + return m.Err +} + +func (m *mockClusterManager) CreateCluster(provider string, inp moduleLib.Cluster, inq moduleLib.ClusterContent) (moduleLib.Cluster, error) { + if m.Err != nil { + return moduleLib.Cluster{}, m.Err + } + + return m.ClusterItems[0], nil +} + +func (m *mockClusterManager) GetCluster(provider, name string) (moduleLib.Cluster, error) { + if m.Err != nil { + return moduleLib.Cluster{}, m.Err + } + + return m.ClusterItems[0], nil +} + +func (m *mockClusterManager) GetClusterContent(provider, name string) (moduleLib.ClusterContent, error) { + if m.Err != nil { + return moduleLib.ClusterContent{}, m.Err + } + + return m.ClusterContentItems[0], nil +} + +func (m *mockClusterManager) GetClusters(provider string) ([]moduleLib.Cluster, error) { + if m.Err != nil { + return []moduleLib.Cluster{}, m.Err + } + + return m.ClusterItems, nil +} + +func (m *mockClusterManager) DeleteCluster(provider, name string) error { + return m.Err +} + +func (m *mockClusterManager) CreateClusterLabel(provider, cluster string, inp moduleLib.ClusterLabel) (moduleLib.ClusterLabel, error) { + if m.Err != nil { + return moduleLib.ClusterLabel{}, m.Err + } + + return m.ClusterLabelItems[0], nil +} + +func (m *mockClusterManager) GetClusterLabel(provider, cluster, label string) (moduleLib.ClusterLabel, error) { + if m.Err != nil { + return moduleLib.ClusterLabel{}, m.Err + } + + return m.ClusterLabelItems[0], nil +} + +func (m *mockClusterManager) GetClusterLabels(provider, cluster string) ([]moduleLib.ClusterLabel, error) { + if m.Err != nil { + return []moduleLib.ClusterLabel{}, m.Err + } + + return m.ClusterLabelItems, nil +} + +func (m *mockClusterManager) DeleteClusterLabel(provider, cluster, label string) error { + return m.Err +} + +func (m *mockClusterManager) CreateClusterKvPairs(provider, cluster string, inp moduleLib.ClusterKvPairs) (moduleLib.ClusterKvPairs, error) { + if m.Err != nil { + return moduleLib.ClusterKvPairs{}, m.Err + } + + return m.ClusterKvPairsItems[0], nil +} + +func (m *mockClusterManager) GetClusterKvPairs(provider, cluster, kvpair string) (moduleLib.ClusterKvPairs, error) { + if m.Err != nil { + return moduleLib.ClusterKvPairs{}, m.Err + } + + return m.ClusterKvPairsItems[0], nil +} + +func (m *mockClusterManager) GetAllClusterKvPairs(provider, cluster string) ([]moduleLib.ClusterKvPairs, error) { + if m.Err != nil { + return []moduleLib.ClusterKvPairs{}, m.Err + } + + return m.ClusterKvPairsItems, nil +} + +func (m *mockClusterManager) DeleteClusterKvPairs(provider, cluster, kvpair string) error { + return m.Err +} + +func TestClusterProviderCreateHandler(t *testing.T) { + testCases := []struct { + label string + reader io.Reader + expected moduleLib.ClusterProvider + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Missing Cluster Provider Body Failure", + expectedCode: http.StatusBadRequest, + clusterClient: &mockClusterManager{}, + }, + { + label: "Create Cluster Provider", + expectedCode: http.StatusCreated, + reader: bytes.NewBuffer([]byte(`{ + "metadata": { + "name": "clusterProviderTest", + "description": "testClusterProvider", + "userData1": "some user data 1", + "userData2": "some user data 2" + } + }`)), + expected: moduleLib.ClusterProvider{ + Metadata: moduleLib.Metadata{ + Name: "clusterProviderTest", + Description: "testClusterProvider", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterProviderItems: []moduleLib.ClusterProvider{ + { + Metadata: moduleLib.Metadata{ + Name: "clusterProviderTest", + Description: "testClusterProvider", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + }, + }, + }, + { + label: "Missing ClusterProvider Name in Request Body", + reader: bytes.NewBuffer([]byte(`{ + "metadata": { + "description": "this is a test cluster provider", + "userData1": "some user data 1", + "userData2": "some user data 2" + } + }`)), + expectedCode: http.StatusBadRequest, + clusterClient: &mockClusterManager{}, + }, + } + + 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(testCase.clusterClient)) + + //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.ClusterProvider{} + 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) + } + } + }) + } +} + +func TestClusterProviderGetAllHandler(t *testing.T) { + + testCases := []struct { + label string + expected []moduleLib.ClusterProvider + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster Provider", + expectedCode: http.StatusOK, + expected: []moduleLib.ClusterProvider{ + { + Metadata: moduleLib.Metadata{ + Name: "testClusterProvider1", + Description: "testClusterProvider 1 description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + { + Metadata: moduleLib.Metadata{ + Name: "testClusterProvider2", + Description: "testClusterProvider 2 description", + UserData1: "some user data A", + UserData2: "some user data B", + }, + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterProviderItems: []moduleLib.ClusterProvider{ + { + Metadata: moduleLib.Metadata{ + Name: "testClusterProvider1", + Description: "testClusterProvider 1 description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + { + Metadata: moduleLib.Metadata{ + Name: "testClusterProvider2", + Description: "testClusterProvider 2 description", + UserData1: "some user data A", + UserData2: "some user data B", + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v2/cluster-providers", nil) + resp := executeRequest(request, NewRouter(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := []moduleLib.ClusterProvider{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterProviderGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected moduleLib.ClusterProvider + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster Provider", + expectedCode: http.StatusOK, + expected: moduleLib.ClusterProvider{ + Metadata: moduleLib.Metadata{ + Name: "testClusterProvider", + Description: "testClusterProvider description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + name: "testClusterProvider", + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterProviderItems: []moduleLib.ClusterProvider{ + { + Metadata: moduleLib.Metadata{ + Name: "testClusterProvider", + Description: "testClusterProvider description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + }, + }, + }, + { + label: "Get Non-Existing Cluster Provider", + expectedCode: http.StatusInternalServerError, + name: "nonexistingclusterprovider", + clusterClient: &mockClusterManager{ + ClusterProviderItems: []moduleLib.ClusterProvider{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.ClusterProvider{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterProviderDeleteHandler(t *testing.T) { + + testCases := []struct { + label string + name string + version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Delete Cluster Provider", + expectedCode: http.StatusNoContent, + name: "testClusterProvider", + clusterClient: &mockClusterManager{}, + }, + { + label: "Delete Non-Existing Cluster Provider", + expectedCode: http.StatusInternalServerError, + name: "testClusterProvider", + clusterClient: &mockClusterManager{ + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + }) + } +} + +func TestClusterCreateHandler(t *testing.T) { + testCases := []struct { + label string + metadata string + kubeconfig string + expected moduleLib.Cluster + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Missing Cluster Body Failure", + expectedCode: http.StatusBadRequest, + clusterClient: &mockClusterManager{}, + }, + { + label: "Create Cluster", + expectedCode: http.StatusCreated, + metadata: ` +{ + "metadata": { + "name": "clusterTest", + "description": "this is test cluster", + "userData1": "some cluster data abc", + "userData2": "some cluster data def" + } +}`, + kubeconfig: `test contents +of a file attached +to the creation +of clusterTest +`, + expected: moduleLib.Cluster{ + Metadata: moduleLib.Metadata{ + Name: "clusterTest", + Description: "testCluster", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterProviderItems: []moduleLib.ClusterProvider{ + { + Metadata: moduleLib.Metadata{ + Name: "clusterProvider1", + Description: "ClusterProvider 1 description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + }, + ClusterItems: []moduleLib.Cluster{ + { + Metadata: moduleLib.Metadata{ + Name: "clusterTest", + Description: "testCluster", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + }, + ClusterContentItems: []moduleLib.ClusterContent{ + { + Kubeconfig: "dGVzdCBjb250ZW50cwpvZiBhIGZpbGUgYXR0YWNoZWQKdG8gdGhlIGNyZWF0aW9uCm9mIGNsdXN0ZXJUZXN0Cg==", + }, + }, + }, + }, + { + label: "Missing Cluster Name in Request Body", + expectedCode: http.StatusBadRequest, + metadata: ` +{ + "metadata": { + "description": "this is test cluster", + "userData1": "some cluster data abc", + "userData2": "some cluster data def" + } +}`, + kubeconfig: `test contents +of a file attached +to the creation +of clusterTest +`, + clusterClient: &mockClusterManager{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + // Create the multipart test Request body + body := new(bytes.Buffer) + multiwr := multipart.NewWriter(body) + multiwr.SetBoundary("------------------------f77f80a7092eb312") + pw, _ := multiwr.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/json"}, "Content-Disposition": {"form-data; name=metadata"}}) + pw.Write([]byte(testCase.metadata)) + pw, _ = multiwr.CreateFormFile("file", "kubeconfig") + pw.Write([]byte(testCase.kubeconfig)) + multiwr.Close() + + request := httptest.NewRequest("POST", "/v2/cluster-providers/clusterProvider1/clusters", bytes.NewBuffer(body.Bytes())) + request.Header.Set("Content-Type", multiwr.FormDataContentType()) + resp := executeRequest(request, NewRouter(testCase.clusterClient)) + + //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.Cluster{} + 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) + } + } + }) + } +} + +func TestClusterGetAllHandler(t *testing.T) { + + testCases := []struct { + label string + expected []moduleLib.Cluster + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Clusters", + expectedCode: http.StatusOK, + expected: []moduleLib.Cluster{ + { + Metadata: moduleLib.Metadata{ + Name: "testCluster1", + Description: "testCluster 1 description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + { + Metadata: moduleLib.Metadata{ + Name: "testCluster2", + Description: "testCluster 2 description", + UserData1: "some user data A", + UserData2: "some user data B", + }, + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterItems: []moduleLib.Cluster{ + { + Metadata: moduleLib.Metadata{ + Name: "testCluster1", + Description: "testCluster 1 description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + { + Metadata: moduleLib.Metadata{ + Name: "testCluster2", + Description: "testCluster 2 description", + UserData1: "some user data A", + UserData2: "some user data B", + }, + }, + }, + ClusterContentItems: []moduleLib.ClusterContent{ + // content here doesn't matter - just needs to be present + { + Kubeconfig: "dGVzdCBjb250ZW50cwpvZiBhIGZpbGUgYXR0YWNoZWQKdG8gdGhlIGNyZWF0aW9uCm9mIGNsdXN0ZXJUZXN0Cg==", + }, + { + Kubeconfig: "dGVzdCBjb250ZW50cwpvZiBhIGZpbGUgYXR0YWNoZWQKdG8gdGhlIGNyZWF0aW9uCm9mIGNsdXN0ZXJUZXN0Cg==", + }, + }, + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := []moduleLib.Cluster{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected moduleLib.Cluster + name, version string + accept string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster with Accept: application/json", + accept: "application/json", + expectedCode: http.StatusOK, + expected: moduleLib.Cluster{ + Metadata: moduleLib.Metadata{ + Name: "testCluster", + Description: "testCluster description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + name: "testCluster", + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterItems: []moduleLib.Cluster{ + { + Metadata: moduleLib.Metadata{ + Name: "testCluster", + Description: "testCluster description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + }, + ClusterContentItems: []moduleLib.ClusterContent{ + { + Kubeconfig: "dGVzdCBjb250ZW50cwpvZiBhIGZpbGUgYXR0YWNoZWQKdG8gdGhlIGNyZWF0aW9uCm9mIGNsdXN0ZXJUZXN0Cg==", + }, + }, + }, + }, + { + label: "Get Non-Existing Cluster", + accept: "application/json", + expectedCode: http.StatusInternalServerError, + name: "nonexistingcluster", + clusterClient: &mockClusterManager{ + ClusterItems: []moduleLib.Cluster{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvider1/clusters/"+testCase.name, nil) + if len(testCase.accept) > 0 { + request.Header.Set("Accept", testCase.accept) + } + resp := executeRequest(request, NewRouter(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.Cluster{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterGetContentHandler(t *testing.T) { + + testCases := []struct { + label string + expected string + name, version string + accept string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster Content with Accept: application/octet-stream", + accept: "application/octet-stream", + expectedCode: http.StatusOK, + expected: `test contents +of a file attached +to the creation +of clusterTest +`, + name: "testCluster", + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterItems: []moduleLib.Cluster{ + { + Metadata: moduleLib.Metadata{ + Name: "testCluster", + Description: "testCluster description", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + }, + }, + ClusterContentItems: []moduleLib.ClusterContent{ + { + Kubeconfig: "dGVzdCBjb250ZW50cwpvZiBhIGZpbGUgYXR0YWNoZWQKdG8gdGhlIGNyZWF0aW9uCm9mIGNsdXN0ZXJUZXN0Cg==", + }, + }, + }, + }, + { + label: "Get Non-Existing Cluster", + accept: "application/octet-stream", + expectedCode: http.StatusInternalServerError, + name: "nonexistingcluster", + clusterClient: &mockClusterManager{ + ClusterItems: []moduleLib.Cluster{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v2/cluster-providers/clusterProvider1/clusters/"+testCase.name, nil) + if len(testCase.accept) > 0 { + request.Header.Set("Accept", testCase.accept) + } + resp := executeRequest(request, NewRouter(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + body := new(bytes.Buffer) + body.ReadFrom(resp.Body) + got := body.String() + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterDeleteHandler(t *testing.T) { + + testCases := []struct { + label string + name string + version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Delete Cluster", + expectedCode: http.StatusNoContent, + name: "testCluster", + clusterClient: &mockClusterManager{}, + }, + { + label: "Delete Non-Existing Cluster", + expectedCode: http.StatusInternalServerError, + name: "testCluster", + clusterClient: &mockClusterManager{ + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + }) + } +} + +func TestClusterLabelCreateHandler(t *testing.T) { + testCases := []struct { + label string + reader io.Reader + expected moduleLib.ClusterLabel + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Missing Cluster Label Body Failure", + expectedCode: http.StatusBadRequest, + clusterClient: &mockClusterManager{}, + }, + { + label: "Create Cluster Label", + expectedCode: http.StatusCreated, + reader: bytes.NewBuffer([]byte(`{ + "label-name": "test-label" + }`)), + expected: moduleLib.ClusterLabel{ + LabelName: "test-label", + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterLabelItems: []moduleLib.ClusterLabel{ + { + LabelName: "test-label", + }, + }, + }, + }, + } + + 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(testCase.clusterClient)) + + //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.ClusterLabel{} + 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) + } + } + }) + } +} + +func TestClusterLabelsGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected []moduleLib.ClusterLabel + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster Labels", + expectedCode: http.StatusOK, + expected: []moduleLib.ClusterLabel{ + { + LabelName: "test-label1", + }, + { + LabelName: "test-label-two", + }, + { + LabelName: "test-label-3", + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterLabelItems: []moduleLib.ClusterLabel{ + { + LabelName: "test-label1", + }, + { + LabelName: "test-label-two", + }, + { + LabelName: "test-label-3", + }, + }, + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := []moduleLib.ClusterLabel{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterLabelGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected moduleLib.ClusterLabel + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster Label", + expectedCode: http.StatusOK, + expected: moduleLib.ClusterLabel{ + LabelName: "testlabel", + }, + name: "testlabel", + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterLabelItems: []moduleLib.ClusterLabel{ + { + LabelName: "testlabel", + }, + }, + }, + }, + { + label: "Get Non-Existing Cluster Label", + expectedCode: http.StatusInternalServerError, + name: "nonexistingclusterlabel", + clusterClient: &mockClusterManager{ + ClusterLabelItems: []moduleLib.ClusterLabel{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.ClusterLabel{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterLabelDeleteHandler(t *testing.T) { + + testCases := []struct { + label string + name string + version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Delete Cluster Label", + expectedCode: http.StatusNoContent, + name: "testClusterLabel", + clusterClient: &mockClusterManager{}, + }, + { + label: "Delete Non-Existing Cluster Label", + expectedCode: http.StatusInternalServerError, + name: "testClusterLabel", + clusterClient: &mockClusterManager{ + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + }) + } +} + +func TestClusterKvPairsCreateHandler(t *testing.T) { + testCases := []struct { + label string + reader io.Reader + expected moduleLib.ClusterKvPairs + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Missing Cluster KvPairs Body Failure", + expectedCode: http.StatusBadRequest, + clusterClient: &mockClusterManager{}, + }, + { + label: "Create Cluster KvPairs", + expectedCode: http.StatusCreated, + reader: bytes.NewBuffer([]byte(`{ + "metadata": { + "name": "ClusterKvPair1", + "description": "test cluster kv pairs", + "userData1": "some user data 1", + "userData2": "some user data 2" + }, + "spec": { + "kv": [ + { + "key1": "value1" + }, + { + "key2": "value2" + } + ] + } + }`)), + expected: moduleLib.ClusterKvPairs{ + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair1", + Description: "test cluster kv pairs", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "key1": "value1", + }, + { + "key2": "value2", + }, + }, + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterKvPairsItems: []moduleLib.ClusterKvPairs{ + { + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair1", + Description: "test cluster kv pairs", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "key1": "value1", + }, + { + "key2": "value2", + }, + }, + }, + }, + }, + }, + }, + } + + 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(testCase.clusterClient)) + + //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.ClusterKvPairs{} + 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) + } + } + }) + } +} + +func TestClusterKvPairsGetAllHandler(t *testing.T) { + + testCases := []struct { + label string + expected []moduleLib.ClusterKvPairs + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster KvPairs", + expectedCode: http.StatusOK, + expected: []moduleLib.ClusterKvPairs{ + { + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair1", + Description: "test cluster kv pairs", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "key1": "value1", + }, + { + "key2": "value2", + }, + }, + }, + }, + { + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair2", + Description: "test cluster kv pairs", + UserData1: "some user data A", + UserData2: "some user data B", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "keyA": "valueA", + }, + { + "keyB": "valueB", + }, + }, + }, + }, + }, + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterKvPairsItems: []moduleLib.ClusterKvPairs{ + { + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair1", + Description: "test cluster kv pairs", + UserData1: "some user data 1", + UserData2: "some user data 2", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "key1": "value1", + }, + { + "key2": "value2", + }, + }, + }, + }, + { + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair2", + Description: "test cluster kv pairs", + UserData1: "some user data A", + UserData2: "some user data B", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "keyA": "valueA", + }, + { + "keyB": "valueB", + }, + }, + }, + }, + }, + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := []moduleLib.ClusterKvPairs{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterKvPairsGetHandler(t *testing.T) { + + testCases := []struct { + label string + expected moduleLib.ClusterKvPairs + name, version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Get Cluster KV Pairs", + expectedCode: http.StatusOK, + expected: moduleLib.ClusterKvPairs{ + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair2", + Description: "test cluster kv pairs", + UserData1: "some user data A", + UserData2: "some user data B", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "keyA": "valueA", + }, + { + "keyB": "valueB", + }, + }, + }, + }, + name: "ClusterKvPair2", + clusterClient: &mockClusterManager{ + //Items that will be returned by the mocked Client + ClusterKvPairsItems: []moduleLib.ClusterKvPairs{ + { + Metadata: moduleLib.Metadata{ + Name: "ClusterKvPair2", + Description: "test cluster kv pairs", + UserData1: "some user data A", + UserData2: "some user data B", + }, + Spec: moduleLib.ClusterKvSpec{ + Kv: []map[string]interface{}{ + { + "keyA": "valueA", + }, + { + "keyB": "valueB", + }, + }, + }, + }, + }, + }, + }, + { + label: "Get Non-Existing Cluster KV Pairs", + expectedCode: http.StatusInternalServerError, + name: "nonexistingclusterkvpairs", + clusterClient: &mockClusterManager{ + ClusterKvPairsItems: []moduleLib.ClusterKvPairs{}, + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.ClusterKvPairs{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("listHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestClusterKvPairsDeleteHandler(t *testing.T) { + + testCases := []struct { + label string + name string + version string + expectedCode int + clusterClient *mockClusterManager + }{ + { + label: "Delete Cluster KV Pairs", + expectedCode: http.StatusNoContent, + name: "testClusterKvPairs", + clusterClient: &mockClusterManager{}, + }, + { + label: "Delete Non-Existing Cluster KV Pairs", + expectedCode: http.StatusInternalServerError, + name: "testClusterKvPairs", + clusterClient: &mockClusterManager{ + Err: pkgerrors.New("Internal Error"), + }, + }, + } + + 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(testCase.clusterClient)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + }) + } +} diff --git a/src/ncm/api/networkhandler.go b/src/ncm/api/networkhandler.go new file mode 100644 index 00000000..01d077a7 --- /dev/null +++ b/src/ncm/api/networkhandler.go @@ -0,0 +1,215 @@ +/* + * 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" + "fmt" + "io" + "net/http" + + moduleLib "github.com/onap/multicloud-k8s/src/ncm/pkg/module" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/validation" + pkgerrors "github.com/pkg/errors" + + "github.com/gorilla/mux" +) + +// Used to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type networkHandler struct { + // Interface that implements Cluster operations + // We will set this variable with a mock interface for testing + client moduleLib.NetworkManager +} + +// Check for valid format of input parameters +func validateNetworkInputs(p moduleLib.Network) error { + // validate name + errs := validation.IsValidName(p.Metadata.Name) + if len(errs) > 0 { + return pkgerrors.Errorf("Invalid network name - name=[%v], errors: %v", p.Metadata.Name, errs) + } + + // validate cni type + found := false + for _, val := range moduleLib.CNI_TYPES { + if p.Spec.CniType == val { + found = true + break + } + } + if !found { + return pkgerrors.Errorf("Invalid cni type: %v", p.Spec.CniType) + } + + subnets := p.Spec.Ipv4Subnets + for _, subnet := range subnets { + err := moduleLib.ValidateSubnet(subnet) + if err != nil { + return pkgerrors.Wrap(err, "invalid subnet") + } + } + return nil +} + +// Create handles creation of the Network entry in the database +func (h networkHandler) createNetworkHandler(w http.ResponseWriter, r *http.Request) { + var p moduleLib.Network + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + + err := json.NewDecoder(r.Body).Decode(&p) + + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + err = validateNetworkInputs(p) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + ret, err := h.client.CreateNetwork(p, clusterProvider, cluster, false) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Put handles creation/update of the Network entry in the database +func (h networkHandler) putNetworkHandler(w http.ResponseWriter, r *http.Request) { + var p moduleLib.Network + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + name := vars["name"] + + err := json.NewDecoder(r.Body).Decode(&p) + + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing name in PUT request", http.StatusBadRequest) + return + } + + // Name in URL should match name in body + if p.Metadata.Name != name { + fmt.Printf("bodyname = %v, name= %v\n", p.Metadata.Name, name) + http.Error(w, "Mismatched name in PUT request", http.StatusBadRequest) + return + } + + err = validateNetworkInputs(p) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + ret, err := h.client.CreateNetwork(p, clusterProvider, cluster, true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular Network Name +// Returns a Network +func (h networkHandler) getNetworkHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + name := vars["name"] + var ret interface{} + var err error + + if len(name) == 0 { + ret, err = h.client.GetNetworks(clusterProvider, cluster) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + ret, err = h.client.GetNetwork(name, clusterProvider, cluster) + 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(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular Network Name +func (h networkHandler) deleteNetworkHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + name := vars["name"] + + err := h.client.DeleteNetwork(name, clusterProvider, cluster) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/ncm/api/providernethandler.go b/src/ncm/api/providernethandler.go new file mode 100644 index 00000000..b38a16c5 --- /dev/null +++ b/src/ncm/api/providernethandler.go @@ -0,0 +1,268 @@ +/* + * 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" + "fmt" + "io" + "net/http" + "strings" + + moduleLib "github.com/onap/multicloud-k8s/src/ncm/pkg/module" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/validation" + pkgerrors "github.com/pkg/errors" + + "github.com/gorilla/mux" +) + +// Used to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type providernetHandler struct { + // Interface that implements Cluster operations + // We will set this variable with a mock interface for testing + client moduleLib.ProviderNetManager +} + +// Check for valid format of input parameters +func validateProviderNetInputs(p moduleLib.ProviderNet) error { + // validate name + errs := validation.IsValidName(p.Metadata.Name) + if len(errs) > 0 { + return pkgerrors.Errorf("Invalid provider network name=[%v], errors: %v", p.Metadata.Name, errs) + } + + // validate cni type + found := false + for _, val := range moduleLib.CNI_TYPES { + if p.Spec.CniType == val { + found = true + break + } + } + if !found { + return pkgerrors.Errorf("Invalid cni type: %v", p.Spec.CniType) + } + + // validate the provider network type + found = false + for _, val := range moduleLib.PROVIDER_NET_TYPES { + if strings.ToUpper(p.Spec.ProviderNetType) == val { + found = true + break + } + } + if !found { + return pkgerrors.Errorf("Invalid provider network type: %v", p.Spec.ProviderNetType) + } + + // validate the subnets + subnets := p.Spec.Ipv4Subnets + for _, subnet := range subnets { + err := moduleLib.ValidateSubnet(subnet) + if err != nil { + return pkgerrors.Wrap(err, "invalid subnet") + } + } + + // validate the VLAN ID + errs = validation.IsValidNumber(p.Spec.Vlan.VlanId, 0, 4095) + if len(errs) > 0 { + return pkgerrors.Errorf("Invalid VlAN ID %v - error: %v", p.Spec.Vlan.VlanId, errs) + } + + // validate the VLAN Node Selector value + expectLabels := false + found = false + for _, val := range moduleLib.VLAN_NODE_SELECTORS { + if strings.ToLower(p.Spec.Vlan.VlanNodeSelector) == val { + found = true + if val == moduleLib.VLAN_NODE_SPECIFIC { + expectLabels = true + } + break + } + } + if !found { + return pkgerrors.Errorf("Invalid VlAN Node Selector %v", p.Spec.Vlan.VlanNodeSelector) + } + + // validate the node label list + gotLabels := false + for _, label := range p.Spec.Vlan.NodeLabelList { + errs = validation.IsValidLabel(label) + if len(errs) > 0 { + return pkgerrors.Errorf("Invalid Label=%v - errors: %v", label, errs) + } + gotLabels = true + } + + // Need at least one label if node selector value was "specific" + // (if selector is "any" - don't care if labels were supplied or not + if expectLabels && !gotLabels { + return pkgerrors.Errorf("Node Labels required for VlAN node selector \"%v\"", moduleLib.VLAN_NODE_SPECIFIC) + } + + return nil +} + +// Create handles creation of the ProviderNet entry in the database +func (h providernetHandler) createProviderNetHandler(w http.ResponseWriter, r *http.Request) { + var p moduleLib.ProviderNet + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + + err := json.NewDecoder(r.Body).Decode(&p) + + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + err = validateProviderNetInputs(p) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + ret, err := h.client.CreateProviderNet(p, clusterProvider, cluster, false) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Put handles creation/update of the ProviderNet entry in the database +func (h providernetHandler) putProviderNetHandler(w http.ResponseWriter, r *http.Request) { + var p moduleLib.ProviderNet + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + name := vars["name"] + + err := json.NewDecoder(r.Body).Decode(&p) + + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if p.Metadata.Name == "" { + http.Error(w, "Missing name in PUT request", http.StatusBadRequest) + return + } + + // Name in URL should match name in body + if p.Metadata.Name != name { + fmt.Printf("bodyname = %v, name= %v\n", p.Metadata.Name, name) + http.Error(w, "Mismatched name in PUT request", http.StatusBadRequest) + return + } + + err = validateProviderNetInputs(p) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + ret, err := h.client.CreateProviderNet(p, clusterProvider, cluster, true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Get handles GET operations on a particular ProviderNet Name +// Returns a ProviderNet +func (h providernetHandler) getProviderNetHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + name := vars["name"] + var ret interface{} + var err error + + if len(name) == 0 { + ret, err = h.client.GetProviderNets(clusterProvider, cluster) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + ret, err = h.client.GetProviderNet(name, clusterProvider, cluster) + 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(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// Delete handles DELETE operations on a particular ProviderNet Name +func (h providernetHandler) deleteProviderNetHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterProvider := vars["provider-name"] + cluster := vars["cluster-name"] + name := vars["name"] + + err := h.client.DeleteProviderNet(name, clusterProvider, cluster) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/ncm/api/testing.go b/src/ncm/api/testing.go new file mode 100644 index 00000000..fed5c080 --- /dev/null +++ b/src/ncm/api/testing.go @@ -0,0 +1,31 @@ +/* + * 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 ( + "net/http" + "net/http/httptest" + + "github.com/gorilla/mux" +) + +func executeRequest(request *http.Request, router *mux.Router) *http.Response { + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, request) + resp := recorder.Result() + return resp +} diff --git a/src/ncm/cmd/main.go b/src/ncm/cmd/main.go new file mode 100644 index 00000000..c4ae423f --- /dev/null +++ b/src/ncm/cmd/main.go @@ -0,0 +1,77 @@ +/* +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. +*/ + +package main + +import ( + "context" + "log" + "math/rand" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/handlers" + "github.com/onap/multicloud-k8s/src/ncm/api" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/auth" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/config" + contextDb "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/contextdb" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" +) + +func main() { + + rand.Seed(time.Now().UnixNano()) + + err := db.InitializeDatabaseConnection("mco") + if err != nil { + log.Println("Unable to initialize database connection...") + log.Println(err) + log.Fatalln("Exiting...") + } + err = contextDb.InitializeContextDatabase() + if err != nil { + log.Println("Unable to initialize database connection...") + log.Println(err) + log.Fatalln("Exiting...") + } + + httpRouter := api.NewRouter(nil) + loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) + log.Println("Starting Network Customization Manager") + + httpServer := &http.Server{ + Handler: loggedRouter, + Addr: ":" + config.GetConfiguration().ServicePort, + } + + connectionsClose := make(chan struct{}) + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + httpServer.Shutdown(context.Background()) + close(connectionsClose) + }() + + tlsConfig, err := auth.GetTLSConfig("ca.cert", "server.cert", "server.key") + if err != nil { + log.Println("Error Getting TLS Configuration. Starting without TLS...") + log.Fatal(httpServer.ListenAndServe()) + } else { + httpServer.TLSConfig = tlsConfig + // empty strings because tlsconfig already has this information + err = httpServer.ListenAndServeTLS("", "") + } +} diff --git a/src/ncm/go.mod b/src/ncm/go.mod new file mode 100644 index 00000000..32ff481a --- /dev/null +++ b/src/ncm/go.mod @@ -0,0 +1,31 @@ +module github.com/onap/multicloud-k8s/src/ncm + +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/gorilla/handlers v1.3.0 + github.com/gorilla/mux v1.6.2 + github.com/hashicorp/consul v1.4.0 + github.com/pkg/errors v0.8.1 + github.com/sirupsen/logrus v1.4.2 + go.etcd.io/etcd v3.3.12+incompatible + go.mongodb.org/mongo-driver v1.0.0 + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 + k8s.io/api v0.0.0-20190831074750-7364b6bdad65 + k8s.io/apimachinery v0.0.0-20190831074630-461753078381 + k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible + k8s.io/helm v2.14.3+incompatible +) + +replace ( + k8s.io/api => k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8 + k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d + k8s.io/apiserver => k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c + k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190409023024-d644b00f3b79 + 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/ncm/pkg/module/cluster.go b/src/ncm/pkg/module/cluster.go new file mode 100644 index 00000000..e3260b7e --- /dev/null +++ b/src/ncm/pkg/module/cluster.go @@ -0,0 +1,532 @@ +/* + * 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" +) + +// ClusterProvider contains the parameters needed for ClusterProviders +type ClusterProvider struct { + Metadata Metadata `json:"metadata"` +} + +type Cluster struct { + Metadata Metadata `json:"metadata"` +} + +type ClusterContent struct { + Kubeconfig string `json:"kubeconfig"` +} + +type ClusterLabel struct { + LabelName string `json:"label-name"` +} + +type ClusterKvPairs struct { + Metadata Metadata `json:"metadata"` + Spec ClusterKvSpec `json:"spec"` +} + +type ClusterKvSpec struct { + Kv []map[string]interface{} `json:"kv"` +} + +// ClusterProviderKey is the key structure that is used in the database +type ClusterProviderKey struct { + ClusterProviderName string `json:"provider"` +} + +// ClusterKey is the key structure that is used in the database +type ClusterKey struct { + ClusterProviderName string `json:"provider"` + ClusterName string `json:"cluster"` +} + +// ClusterLabelKey is the key structure that is used in the database +type ClusterLabelKey struct { + ClusterProviderName string `json:"provider"` + ClusterName string `json:"cluster"` + ClusterLabelName string `json:"label"` +} + +// ClusterKvPairsKey is the key structure that is used in the database +type ClusterKvPairsKey struct { + ClusterProviderName string `json:"provider"` + ClusterName string `json:"cluster"` + ClusterKvPairsName string `json:"kvname"` +} + +// Manager is an interface exposes the Cluster functionality +type ClusterManager interface { + CreateClusterProvider(pr ClusterProvider) (ClusterProvider, error) + GetClusterProvider(name string) (ClusterProvider, error) + GetClusterProviders() ([]ClusterProvider, error) + DeleteClusterProvider(name string) error + CreateCluster(provider string, pr Cluster, qr ClusterContent) (Cluster, error) + GetCluster(provider, name string) (Cluster, error) + GetClusterContent(provider, name string) (ClusterContent, error) + GetClusters(provider string) ([]Cluster, error) + DeleteCluster(provider, name string) error + CreateClusterLabel(provider, cluster string, pr ClusterLabel) (ClusterLabel, error) + GetClusterLabel(provider, cluster, label string) (ClusterLabel, error) + GetClusterLabels(provider, cluster string) ([]ClusterLabel, error) + DeleteClusterLabel(provider, cluster, label string) error + CreateClusterKvPairs(provider, cluster string, pr ClusterKvPairs) (ClusterKvPairs, error) + GetClusterKvPairs(provider, cluster, kvpair string) (ClusterKvPairs, error) + GetAllClusterKvPairs(provider, cluster string) ([]ClusterKvPairs, error) + DeleteClusterKvPairs(provider, cluster, kvpair string) error +} + +// ClusterClient implements the Manager +// It will also be used to maintain some localized state +type ClusterClient struct { + db ClientDbInfo +} + +// NewClusterClient returns an instance of the ClusterClient +// which implements the Manager +func NewClusterClient() *ClusterClient { + return &ClusterClient{ + db: ClientDbInfo{ + storeName: "cluster", + tagMeta: "clustermetadata", + tagContent: "clustercontent", + }, + } +} + +// CreateClusterProvider - create a new Cluster Provider +func (v *ClusterClient) CreateClusterProvider(p ClusterProvider) (ClusterProvider, error) { + + //Construct key and tag to select the entry + key := ClusterProviderKey{ + ClusterProviderName: p.Metadata.Name, + } + + //Check if this ClusterProvider already exists + _, err := v.GetClusterProvider(p.Metadata.Name) + if err == nil { + return ClusterProvider{}, pkgerrors.New("ClusterProvider already exists") + } + + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagMeta, p) + if err != nil { + return ClusterProvider{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetClusterProvider returns the ClusterProvider for corresponding name +func (v *ClusterClient) GetClusterProvider(name string) (ClusterProvider, error) { + + //Construct key and tag to select the entry + key := ClusterProviderKey{ + ClusterProviderName: name, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return ClusterProvider{}, pkgerrors.Wrap(err, "Get ClusterProvider") + } + + //value is a byte array + if value != nil { + cp := ClusterProvider{} + err = db.DBconn.Unmarshal(value[0], &cp) + if err != nil { + return ClusterProvider{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return cp, nil + } + + return ClusterProvider{}, pkgerrors.New("Error getting ClusterProvider") +} + +// GetClusterProviderList returns all of the ClusterProvider for corresponding name +func (v *ClusterClient) GetClusterProviders() ([]ClusterProvider, error) { + + //Construct key and tag to select the entry + key := ClusterProviderKey{ + ClusterProviderName: "", + } + + var resp []ClusterProvider + values, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return []ClusterProvider{}, pkgerrors.Wrap(err, "Get ClusterProviders") + } + + for _, value := range values { + cp := ClusterProvider{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []ClusterProvider{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// DeleteClusterProvider the ClusterProvider from database +func (v *ClusterClient) DeleteClusterProvider(name string) error { + + //Construct key and tag to select the entry + key := ClusterProviderKey{ + ClusterProviderName: name, + } + + err := db.DBconn.Remove(v.db.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete ClusterProvider Entry;") + } + + return nil +} + +// CreateCluster - create a new Cluster for a cluster-provider +func (v *ClusterClient) CreateCluster(provider string, p Cluster, q ClusterContent) (Cluster, error) { + + //Construct key and tag to select the entry + key := ClusterKey{ + ClusterProviderName: provider, + ClusterName: p.Metadata.Name, + } + + //Verify ClusterProvider already exists + _, err := v.GetClusterProvider(provider) + if err != nil { + return Cluster{}, pkgerrors.New("ClusterProvider does not exist") + } + + //Check if this Cluster already exists + _, err = v.GetCluster(provider, p.Metadata.Name) + if err == nil { + return Cluster{}, pkgerrors.New("Cluster already exists") + } + + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagMeta, p) + if err != nil { + return Cluster{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagContent, q) + if err != nil { + return Cluster{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetCluster returns the Cluster for corresponding provider and name +func (v *ClusterClient) GetCluster(provider, name string) (Cluster, error) { + //Construct key and tag to select the entry + key := ClusterKey{ + ClusterProviderName: provider, + ClusterName: name, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return Cluster{}, pkgerrors.Wrap(err, "Get Cluster") + } + + //value is a byte array + if value != nil { + cl := Cluster{} + err = db.DBconn.Unmarshal(value[0], &cl) + if err != nil { + return Cluster{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return cl, nil + } + + return Cluster{}, pkgerrors.New("Error getting Cluster") +} + +// GetClusterContent returns the ClusterContent for corresponding provider and name +func (v *ClusterClient) GetClusterContent(provider, name string) (ClusterContent, error) { + //Construct key and tag to select the entry + key := ClusterKey{ + ClusterProviderName: provider, + ClusterName: name, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagContent) + if err != nil { + return ClusterContent{}, pkgerrors.Wrap(err, "Get Cluster Content") + } + + //value is a byte array + if value != nil { + cc := ClusterContent{} + err = db.DBconn.Unmarshal(value[0], &cc) + if err != nil { + return ClusterContent{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return cc, nil + } + + return ClusterContent{}, pkgerrors.New("Error getting Cluster Content") +} + +// GetClusters returns all the Clusters for corresponding provider +func (v *ClusterClient) GetClusters(provider string) ([]Cluster, error) { + //Construct key and tag to select the entry + key := ClusterKey{ + ClusterProviderName: provider, + ClusterName: "", + } + + values, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return []Cluster{}, pkgerrors.Wrap(err, "Get Clusters") + } + + var resp []Cluster + + for _, value := range values { + cp := Cluster{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []Cluster{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// DeleteCluster the Cluster from database +func (v *ClusterClient) DeleteCluster(provider, name string) error { + //Construct key and tag to select the entry + key := ClusterKey{ + ClusterProviderName: provider, + ClusterName: name, + } + + err := db.DBconn.Remove(v.db.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete Cluster Entry;") + } + + return nil +} + +// CreateClusterLabel - create a new Cluster Label mongo document for a cluster-provider/cluster +func (v *ClusterClient) CreateClusterLabel(provider string, cluster string, p ClusterLabel) (ClusterLabel, error) { + //Construct key and tag to select the entry + key := ClusterLabelKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterLabelName: p.LabelName, + } + + //Verify Cluster already exists + _, err := v.GetCluster(provider, cluster) + if err != nil { + return ClusterLabel{}, pkgerrors.New("Cluster does not exist") + } + + //Check if this ClusterLabel already exists + _, err = v.GetClusterLabel(provider, cluster, p.LabelName) + if err == nil { + return ClusterLabel{}, pkgerrors.New("Cluster Label already exists") + } + + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagMeta, p) + if err != nil { + return ClusterLabel{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetClusterLabel returns the Cluster for corresponding provider, cluster and label +func (v *ClusterClient) GetClusterLabel(provider, cluster, label string) (ClusterLabel, error) { + //Construct key and tag to select the entry + key := ClusterLabelKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterLabelName: label, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return ClusterLabel{}, pkgerrors.Wrap(err, "Get Cluster") + } + + //value is a byte array + if value != nil { + cl := ClusterLabel{} + err = db.DBconn.Unmarshal(value[0], &cl) + if err != nil { + return ClusterLabel{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return cl, nil + } + + return ClusterLabel{}, pkgerrors.New("Error getting Cluster") +} + +// GetClusterLabels returns the Cluster Labels for corresponding provider and cluster +func (v *ClusterClient) GetClusterLabels(provider, cluster string) ([]ClusterLabel, error) { + //Construct key and tag to select the entry + key := ClusterLabelKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterLabelName: "", + } + + values, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return []ClusterLabel{}, pkgerrors.Wrap(err, "Get Cluster Labels") + } + + var resp []ClusterLabel + + for _, value := range values { + cp := ClusterLabel{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []ClusterLabel{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// Delete the Cluster Label from database +func (v *ClusterClient) DeleteClusterLabel(provider, cluster, label string) error { + //Construct key and tag to select the entry + key := ClusterLabelKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterLabelName: label, + } + + err := db.DBconn.Remove(v.db.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete ClusterLabel Entry;") + } + + return nil +} + +// CreateClusterKvPairs - Create a New Cluster KV pairs document +func (v *ClusterClient) CreateClusterKvPairs(provider string, cluster string, p ClusterKvPairs) (ClusterKvPairs, error) { + key := ClusterKvPairsKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterKvPairsName: p.Metadata.Name, + } + + //Verify Cluster already exists + _, err := v.GetCluster(provider, cluster) + if err != nil { + return ClusterKvPairs{}, pkgerrors.New("Cluster does not exist") + } + + //Check if this ClusterKvPairs already exists + _, err = v.GetClusterKvPairs(provider, cluster, p.Metadata.Name) + if err == nil { + return ClusterKvPairs{}, pkgerrors.New("Cluster KV Pair already exists") + } + + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagMeta, p) + if err != nil { + return ClusterKvPairs{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetClusterKvPairs returns the Cluster KeyValue pair for corresponding provider, cluster and KV pair name +func (v *ClusterClient) GetClusterKvPairs(provider, cluster, kvpair string) (ClusterKvPairs, error) { + //Construct key and tag to select entry + key := ClusterKvPairsKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterKvPairsName: kvpair, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return ClusterKvPairs{}, pkgerrors.Wrap(err, "Get Cluster") + } + + //value is a byte array + if value != nil { + ckvp := ClusterKvPairs{} + err = db.DBconn.Unmarshal(value[0], &ckvp) + if err != nil { + return ClusterKvPairs{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return ckvp, nil + } + + return ClusterKvPairs{}, pkgerrors.New("Error getting Cluster") +} + +// GetAllClusterKvPairs returns the Cluster Kv Pairs for corresponding provider and cluster +func (v *ClusterClient) GetAllClusterKvPairs(provider, cluster string) ([]ClusterKvPairs, error) { + //Construct key and tag to select the entry + key := ClusterKvPairsKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterKvPairsName: "", + } + + values, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return []ClusterKvPairs{}, pkgerrors.Wrap(err, "Get Cluster KV Pairs") + } + + var resp []ClusterKvPairs + + for _, value := range values { + cp := ClusterKvPairs{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []ClusterKvPairs{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// DeleteClusterKvPairs the ClusterKvPairs from database +func (v *ClusterClient) DeleteClusterKvPairs(provider, cluster, kvpair string) error { + //Construct key and tag to select entry + key := ClusterKvPairsKey{ + ClusterProviderName: provider, + ClusterName: cluster, + ClusterKvPairsName: kvpair, + } + + err := db.DBconn.Remove(v.db.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete ClusterKvPairs Entry;") + } + + return nil +} diff --git a/src/ncm/pkg/module/module.go b/src/ncm/pkg/module/module.go new file mode 100644 index 00000000..a9950901 --- /dev/null +++ b/src/ncm/pkg/module/module.go @@ -0,0 +1,35 @@ +/* + * 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 + +// Client for using the services in the orchestrator +type Client struct { + Cluster *ClusterClient + Network *NetworkClient + ProviderNet *ProviderNetClient + // Add Clients for API's here +} + +// NewClient creates a new client for using the services +func NewClient() *Client { + c := &Client{} + c.Cluster = NewClusterClient() + c.Network = NewNetworkClient() + c.ProviderNet = NewProviderNetClient() + // Add Client API handlers here + return c +} diff --git a/src/ncm/pkg/module/module_definitions.go b/src/ncm/pkg/module/module_definitions.go new file mode 100644 index 00000000..1e839014 --- /dev/null +++ b/src/ncm/pkg/module/module_definitions.go @@ -0,0 +1,101 @@ +/* + * 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 ( + "strings" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/validation" + pkgerrors "github.com/pkg/errors" +) + +const VLAN_PROVIDER_NET_TYPE_VLAN string = "VLAN" +const VLAN_PROVIDER_NET_TYPE_DIRECT string = "DIRECT" + +var PROVIDER_NET_TYPES = [...]string{VLAN_PROVIDER_NET_TYPE_VLAN, VLAN_PROVIDER_NET_TYPE_DIRECT} + +const CNI_TYPE_OVN4NFV string = "ovn4nfv" + +var CNI_TYPES = [...]string{CNI_TYPE_OVN4NFV} + +// It implements the interface for managing the ClusterProviders +type Metadata struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +type ClientDbInfo struct { + storeName string // name of the mongodb collection to use for client documents + tagMeta string // attribute key name for the json data of a client document + tagContent string // attribute key name for the file data of a client document +} + +type Ipv4Subnet struct { + Subnet string `json:"subnet"` // CIDR notation, e.g. 172.16.33.0/24 + Name string `json:"name"` + Gateway string `json:"gateway"` // IPv4 addre, e.g. 172.16.33.1/24 + Exclude string `json:"excludeIps"` // space separated list of single IPs or ranges e.g. "172.16.33.2 172.16.33.5..172.16.33.10" +} + +const VLAN_NODE_ANY = "any" +const VLAN_NODE_SPECIFIC = "specific" + +var VLAN_NODE_SELECTORS = [...]string{VLAN_NODE_ANY, VLAN_NODE_SPECIFIC} + +type Vlan struct { + VlanId int `json:"vlanID"` + ProviderInterfaceName string `json:"providerInterfaceName"` + LogicalInterfaceName string `json:"logicalInterfaceName"` + VlanNodeSelector string `json:"vlanNodeSelector"` + NodeLabelList []string `json:"nodeLabelList"` +} + +// Check for valid format of an Ipv4Subnet +func ValidateSubnet(sub Ipv4Subnet) error { + // verify subnet is in valid cidr format + err := validation.IsIpv4Cidr(sub.Subnet) + if err != nil { + return pkgerrors.Wrap(err, "invalid subnet") + } + + // just a size check on interface name - system dependent + errs := validation.IsValidName(sub.Name) + if len(errs) > 0 { + return pkgerrors.Errorf("Invalid subnet name=[%v], errors: %v", sub.Name, errs) + } + + // verify gateway is in valid cidr format + if len(sub.Gateway) > 0 { + err = validation.IsIpv4Cidr(sub.Gateway) + if err != nil { + return pkgerrors.Wrap(err, "invalid gateway") + } + } + + // verify excludeIps is composed of space separated ipv4 addresses and + // ipv4 address ranges separated by '..' + for _, value := range strings.Fields(sub.Exclude) { + for _, ip := range strings.SplitN(value, "..", 2) { + err = validation.IsIpv4(ip) + if err != nil { + return pkgerrors.Errorf("invalid ipv4 exclude list %v", sub.Exclude) + } + } + } + return nil +} diff --git a/src/ncm/pkg/module/network.go b/src/ncm/pkg/module/network.go new file mode 100644 index 00000000..2d4121e9 --- /dev/null +++ b/src/ncm/pkg/module/network.go @@ -0,0 +1,170 @@ +/* + * 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" +) + +// Network contains the parameters needed for dynamic networks +type Network struct { + Metadata Metadata `json:"metadata"` + Spec NetworkSpec `json:"spec"` +} + +type NetworkSpec struct { + CniType string `json:"cniType"` + Ipv4Subnets []Ipv4Subnet `json:"ipv4Subnets"` +} + +// NetworkKey is the key structure that is used in the database +type NetworkKey struct { + ClusterProviderName string `json:"provider"` + ClusterName string `json:"cluster"` + NetworkName string `json:"network"` +} + +// Manager is an interface exposing the Network functionality +type NetworkManager interface { + CreateNetwork(pr Network, clusterProvider, cluster string, exists bool) (Network, error) + GetNetwork(name, clusterProvider, cluster string) (Network, error) + GetNetworks(clusterProvider, cluster string) ([]Network, error) + DeleteNetwork(name, clusterProvider, cluster string) error +} + +// NetworkClient implements the Manager +// It will also be used to maintain some localized state +type NetworkClient struct { + db ClientDbInfo +} + +// NewNetworkClient returns an instance of the NetworkClient +// which implements the Manager +func NewNetworkClient() *NetworkClient { + return &NetworkClient{ + db: ClientDbInfo{ + storeName: "cluster", + tagMeta: "networkmetadata", + }, + } +} + +// CreateNetwork - create a new Network +func (v *NetworkClient) CreateNetwork(p Network, clusterProvider, cluster string, exists bool) (Network, error) { + + //Construct key and tag to select the entry + key := NetworkKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + NetworkName: p.Metadata.Name, + } + + //Check if cluster exists + _, err := NewClusterClient().GetCluster(clusterProvider, cluster) + if err != nil { + return Network{}, pkgerrors.New("Unable to find the cluster") + } + + //Check if this Network already exists + _, err = v.GetNetwork(p.Metadata.Name, clusterProvider, cluster) + if err == nil && !exists { + return Network{}, pkgerrors.New("Network already exists") + } + + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagMeta, p) + if err != nil { + return Network{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetNetwork returns the Network for corresponding name +func (v *NetworkClient) GetNetwork(name, clusterProvider, cluster string) (Network, error) { + + //Construct key and tag to select the entry + key := NetworkKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + NetworkName: name, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return Network{}, pkgerrors.Wrap(err, "Get Network") + } + + //value is a byte array + if value != nil { + cp := Network{} + err = db.DBconn.Unmarshal(value[0], &cp) + if err != nil { + return Network{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return cp, nil + } + + return Network{}, pkgerrors.New("Error getting Network") +} + +// GetNetworkList returns all of the Network for corresponding name +func (v *NetworkClient) GetNetworks(clusterProvider, cluster string) ([]Network, error) { + + //Construct key and tag to select the entry + key := NetworkKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + NetworkName: "", + } + + var resp []Network + values, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return []Network{}, pkgerrors.Wrap(err, "Get Networks") + } + + for _, value := range values { + cp := Network{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []Network{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// Delete the Network from database +func (v *NetworkClient) DeleteNetwork(name, clusterProvider, cluster string) error { + + //Construct key and tag to select the entry + key := NetworkKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + NetworkName: name, + } + + err := db.DBconn.Remove(v.db.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete Network Entry;") + } + + return nil +} diff --git a/src/ncm/pkg/module/providernet.go b/src/ncm/pkg/module/providernet.go new file mode 100644 index 00000000..5e2c0343 --- /dev/null +++ b/src/ncm/pkg/module/providernet.go @@ -0,0 +1,172 @@ +/* + * 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" +) + +// ProviderNet contains the parameters needed for dynamic networks +type ProviderNet struct { + Metadata Metadata `json:"metadata"` + Spec ProviderNetSpec `json:"spec"` +} + +type ProviderNetSpec struct { + CniType string `json:"cniType"` + Ipv4Subnets []Ipv4Subnet `json:"ipv4Subnets"` + ProviderNetType string `json:"providerNetType"` + Vlan Vlan `json:"vlan"` +} + +// ProviderNetKey is the key structure that is used in the database +type ProviderNetKey struct { + ClusterProviderName string `json:"provider"` + ClusterName string `json:"cluster"` + ProviderNetName string `json:"providernet"` +} + +// Manager is an interface exposing the ProviderNet functionality +type ProviderNetManager interface { + CreateProviderNet(pr ProviderNet, clusterProvider, cluster string, exists bool) (ProviderNet, error) + GetProviderNet(name, clusterProvider, cluster string) (ProviderNet, error) + GetProviderNets(clusterProvider, cluster string) ([]ProviderNet, error) + DeleteProviderNet(name, clusterProvider, cluster string) error +} + +// ProviderNetClient implements the Manager +// It will also be used to maintain some localized state +type ProviderNetClient struct { + db ClientDbInfo +} + +// NewProviderNetClient returns an instance of the ProviderNetClient +// which implements the Manager +func NewProviderNetClient() *ProviderNetClient { + return &ProviderNetClient{ + db: ClientDbInfo{ + storeName: "cluster", + tagMeta: "networkmetadata", + }, + } +} + +// CreateProviderNet - create a new ProviderNet +func (v *ProviderNetClient) CreateProviderNet(p ProviderNet, clusterProvider, cluster string, exists bool) (ProviderNet, error) { + + //Construct key and tag to select the entry + key := ProviderNetKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + ProviderNetName: p.Metadata.Name, + } + + //Check if cluster exists + _, err := NewClusterClient().GetCluster(clusterProvider, cluster) + if err != nil { + return ProviderNet{}, pkgerrors.New("Unable to find the cluster") + } + + //Check if this ProviderNet already exists + _, err = v.GetProviderNet(p.Metadata.Name, clusterProvider, cluster) + if err == nil && !exists { + return ProviderNet{}, pkgerrors.New("ProviderNet already exists") + } + + err = db.DBconn.Insert(v.db.storeName, key, nil, v.db.tagMeta, p) + if err != nil { + return ProviderNet{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return p, nil +} + +// GetProviderNet returns the ProviderNet for corresponding name +func (v *ProviderNetClient) GetProviderNet(name, clusterProvider, cluster string) (ProviderNet, error) { + + //Construct key and tag to select the entry + key := ProviderNetKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + ProviderNetName: name, + } + + value, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return ProviderNet{}, pkgerrors.Wrap(err, "Get ProviderNet") + } + + //value is a byte array + if value != nil { + cp := ProviderNet{} + err = db.DBconn.Unmarshal(value[0], &cp) + if err != nil { + return ProviderNet{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + return cp, nil + } + + return ProviderNet{}, pkgerrors.New("Error getting ProviderNet") +} + +// GetProviderNetList returns all of the ProviderNet for corresponding name +func (v *ProviderNetClient) GetProviderNets(clusterProvider, cluster string) ([]ProviderNet, error) { + + //Construct key and tag to select the entry + key := ProviderNetKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + ProviderNetName: "", + } + + var resp []ProviderNet + values, err := db.DBconn.Find(v.db.storeName, key, v.db.tagMeta) + if err != nil { + return []ProviderNet{}, pkgerrors.Wrap(err, "Get ProviderNets") + } + + for _, value := range values { + cp := ProviderNet{} + err = db.DBconn.Unmarshal(value, &cp) + if err != nil { + return []ProviderNet{}, pkgerrors.Wrap(err, "Unmarshalling Value") + } + resp = append(resp, cp) + } + + return resp, nil +} + +// Delete the ProviderNet from database +func (v *ProviderNetClient) DeleteProviderNet(name, clusterProvider, cluster string) error { + + //Construct key and tag to select the entry + key := ProviderNetKey{ + ClusterProviderName: clusterProvider, + ClusterName: cluster, + ProviderNetName: name, + } + + err := db.DBconn.Remove(v.db.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete ProviderNet Entry;") + } + + return nil +} diff --git a/src/ncm/scripts/Dockerfile b/src/ncm/scripts/Dockerfile new file mode 100644 index 00000000..d1b58677 --- /dev/null +++ b/src/ncm/scripts/Dockerfile @@ -0,0 +1,30 @@ +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2020 +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +FROM ubuntu:18.04 + +ARG HTTP_PROXY=${HTTP_PROXY} +ARG HTTPS_PROXY=${HTTPS_PROXY} + +ENV http_proxy $HTTP_PROXY +ENV https_proxy $HTTPS_PROXY +ENV no_proxy $NO_PROXY + +EXPOSE 9016 + +RUN groupadd -r onap && useradd -r -g onap onap + +WORKDIR /opt/multicloud/k8s/ncm +RUN chown onap:onap /opt/multicloud/k8s/ncm -R + +ADD --chown=onap ./ncm ./ + +USER onap + +CMD ["./ncm"] diff --git a/src/ncm/tests/certs/auth_test_certificate.pem b/src/ncm/tests/certs/auth_test_certificate.pem new file mode 100644 index 00000000..42e77491 --- /dev/null +++ b/src/ncm/tests/certs/auth_test_certificate.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAKAHJi8eUs73MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgwNTE1MDQ0MDQwWhcNMTkwNTE1MDQ0MDQwWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA5PHDk+RRFh5o3Xe2nZuLn0Vo+5BjnHp/ul2NNYSG00Slc8F86gW4xcNA +wm6xC8tYCSangV2lFG3E8H2L7SCEVaM5VDV2GCOpOoMihc+2Qenk/YbHwuYenwjo +OgTK4aCItqjcAJ2DB1KC7AxARxHBDu9Kif+M/pc49so+G9ObQuS8k2vmTTaRYkMK +ZvbTJcWsc0vbNnPhxcG5PVj4unlaREM+yQDm18/glUkkBNnYKMHABRvPnBrDktTT +BQWsqkbQTw7ZuLOrl9rCzVTstZX9wEXarrstIdQJj3KZjbFOp2opND8bjNIjcdVt +iRFnP1nHQYr7EgRqcx/YMJZ+gmCy3wIDAQABo1AwTjAdBgNVHQ4EFgQU9qPNwwIu +kZh41sJqFtnMC2blSYMwHwYDVR0jBBgwFoAU9qPNwwIukZh41sJqFtnMC2blSYMw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA4+daLY1wE10IMPaOKurG +QBvfYeO/rgNXGeg0TisTIKAfx/We9Hmwo/37Bd2Nk5gxfy/DIJ4lMbrzXjgWITlm +XOrS5QfluwvaEcREtHFtPFa3NZqn2VzKNDFTR+rJj7I5o600NKdcPrGeQ1i/vny2 +K0g68ogw2jfufcuePvZTYGst8RclomPr7ZXxI24kIjcE1MbiViy68sQueTXBEr5s +Th6RsvPfVnLxjR/m/V6VJl31nn4T6hbmKzXCHo/X7aC3I8Isui4bQGKgfAxyBkhE +0T7tP+GgymiEKQ6qJ/1c4HFFSuFRUQjLnK7uJu9jM/HMKoLCPayx6birHZRIMF94 +pg== +-----END CERTIFICATE----- diff --git a/src/ncm/tests/certs/auth_test_key.pem b/src/ncm/tests/certs/auth_test_key.pem new file mode 100644 index 00000000..5f01f572 --- /dev/null +++ b/src/ncm/tests/certs/auth_test_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDk8cOT5FEWHmjd +d7adm4ufRWj7kGOcen+6XY01hIbTRKVzwXzqBbjFw0DCbrELy1gJJqeBXaUUbcTw +fYvtIIRVozlUNXYYI6k6gyKFz7ZB6eT9hsfC5h6fCOg6BMrhoIi2qNwAnYMHUoLs +DEBHEcEO70qJ/4z+lzj2yj4b05tC5LyTa+ZNNpFiQwpm9tMlxaxzS9s2c+HFwbk9 +WPi6eVpEQz7JAObXz+CVSSQE2dgowcAFG8+cGsOS1NMFBayqRtBPDtm4s6uX2sLN +VOy1lf3ARdquuy0h1AmPcpmNsU6naik0PxuM0iNx1W2JEWc/WcdBivsSBGpzH9gw +ln6CYLLfAgMBAAECggEAYB3H2D0QddLKf8AUoNJ+qZ1AV+zkhPtAyIMiF4fN+sBl +HdXrlWxViGFSvM4v8h2qlhzuUfd4qLz042ox5pmyNSnTlbDkJXpDP9dyFO+BOubx +Ribhksd9r5LTvBfq/RKikt0NkAyQx/AyGtuB2NRxUs3PY2QwU2o1dhauQIx0MH5/ +6D8PgQf6+5njKQaKa4e8Kp4kB+KjnALvt6JgYuNJUHWap+nnDbuuVy5dl1bKkAZ+ +qa7CITKWO4kE2EqaCb2asFc2w3538+w72UJZtwQCmOaxtKpRSl9fQXu54N8jIGoZ +1FvEj5x3X6QkglE+iVJYaX3RmiJ3uzZ2LICDr89vEQKBgQD7fquIw4p1idSxz3Cm +5o3Y5kD0CKm61ZaRJWKd+tNlSsxubmV9HROYW6vj2xEPSDvyp1na00pDXxMJQLLc +O5Awd1SaU+d45jnoi70fsEY8X0WH1rDTYfnU+zQBmpbGqX5qTIfpy4yoADiUD1CQ +EBdaSBWiKPx2jFSct58TwDP9YwKBgQDpC64TScZYz7uQq4gAbDso/7TjNwgt/Bw8 +JgLSdx1UdUclh81smTujsouyCFwJSvRjKik8e/Qt0f5patukFbFRINxUGUDhOKbA +7zqeNQbeYaP7Rvw+3z01CU2BTBmB/EWa2xWDam8B9xQvjiHSOrubqkt3sIQJb045 +hzuigdV7VQKBgQD7Gnd0nyCwyMSIIMGuswYv6X4y6i9lr3qdQ4GakOTe/vbsz+cf +K5f0CJuwbnszEgFg/zzVIx/D8rqUA3hSMlp+ObdMO7gi22Q4TsWvTRZjkxBeV7rH +48xJneNIMqyWgIcK5YzSn3y6BTZ4hm3+2UInz09iUJ/6UZTtwNzhIIgIVwKBgQCT +LxRHDE4gIzrT+PHRSonmr/DfnA8nc9WlS2B26lH02IkRs/5Su0iGb6p4y3zNRbCp +vKQElki2c60ZiSqlLCosEfP1jWmDlRMEQVMlPlpTMxmtBr0jPDzc9T4lDhoCFYEk +d3/T2vG3LQRrsHm92+hHPTuioTIS/2BJRxar4RIibQKBgQC8zoayoQ7zfEYxy3Ax +OSao8g85hj0EAJk/VKQP2POgz6KoPay3JE9D7P7OvkebTyv/pijAuTPby4XipCNI +K0JbFC2Kn7RW/ZV23UdnoO9crh2omOh+/52prStWXKoc+/pJe70Af+4rU7FUiI7F +y1mIE9krIoVis6iYsyFEmkP7iw== +-----END PRIVATE KEY----- diff --git a/src/ncm/tests/configs/mock_config.json b/src/ncm/tests/configs/mock_config.json new file mode 100644 index 00000000..47a6b627 --- /dev/null +++ b/src/ncm/tests/configs/mock_config.json @@ -0,0 +1,5 @@ +{ + "database-type": "mock_db_test", + "database-ip": "127.0.0.1", + "plugin-dir": "." +}
\ No newline at end of file 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 f0342433..1d38f106 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -1,34 +1,48 @@ /* -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) *mux.Router { +func NewRouter(projectClient moduleLib.ProjectManager, + compositeAppClient moduleLib.CompositeAppManager, + appClient moduleLib.AppManager, + ControllerClient moduleLib.ControllerManager, + 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, @@ -43,20 +57,111 @@ 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 appClient == nil { + appClient = moduleClient.App + } + appHandler := appHandler{ + client: appClient, + } + + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps", appHandler.createAppHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps/{app-name}", appHandler.getAppHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps", appHandler.getAppHandler).Methods("GET") + router.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/apps/{app-name}", appHandler.deleteAppHandler).Methods("DELETE") + + if compositeProfileClient == nil { + compositeProfileClient = moduleClient.CompositeProfile + } + 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") router.HandleFunc("/controllers/{controller-name}", controlHandler.deleteHandler).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..ef7833de --- /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" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/validation" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + "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 = validation.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/apphandler.go b/src/orchestrator/api/apphandler.go new file mode 100644 index 00000000..2c81431c --- /dev/null +++ b/src/orchestrator/api/apphandler.go @@ -0,0 +1,248 @@ +/* + * 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" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/validation" + moduleLib "github.com/onap/multicloud-k8s/src/orchestrator/pkg/module" + + "github.com/gorilla/mux" +) + +// appHandler to store backend implementations objects +// Also simplifies mocking for unit testing purposes +type appHandler struct { + // Interface that implements App operations + // We will set this variable with a mock interface for testing + client moduleLib.AppManager +} + +// createAppHandler handles creation of the App entry in the database +// This is a multipart handler. See following example curl request +// curl -X POST http://localhost:9015/v2/projects/sampleProject/composite-apps/sampleCompositeApp/v1/apps \ +// -F "metadata={\"metadata\":{\"name\":\"app\",\"description\":\"sample app\",\"UserData1\":\"data1\",\"UserData2\":\"data2\"}};type=application/json" \ +// -F file=@/pathToFile + +func (h appHandler) createAppHandler(w http.ResponseWriter, r *http.Request) { + var a moduleLib.App + var ac moduleLib.AppContent + + // Implemenation using multipart form + // Set Max size to 16mb here + err := r.ParseMultipartForm(16777216) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + jsn := bytes.NewBuffer([]byte(r.FormValue("metadata"))) + err = json.NewDecoder(jsn).Decode(&a) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if a.Metadata.Name == "" { + http.Error(w, "Missing name in POST request", http.StatusBadRequest) + return + } + + //Read the file section and ignore the header + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Unable to process file", http.StatusUnprocessableEntity) + return + } + + defer file.Close() + + //Convert the file content to base64 for storage + content, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, "Unable to read file", http.StatusUnprocessableEntity) + return + } + + err = validation.IsTarGz(bytes.NewBuffer(content)) + if err != nil { + http.Error(w, "Error in file format", http.StatusUnprocessableEntity) + return + } + + ac.FileContent = base64.StdEncoding.EncodeToString(content) + + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + compositeAppVersion := vars["version"] + + ret, err := h.client.CreateApp(a, ac, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// getAppHandler handles GET operations on a particular App Name +// Returns an app +func (h appHandler) getAppHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + compositeAppVersion := vars["version"] + name := vars["app-name"] + + // handle the get all apps case - return a list of only the json parts + if len(name) == 0 { + var retList []moduleLib.App + + ret, err := h.client.GetApps(projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, app := range ret { + retList = append(retList, moduleLib.App{Metadata: app.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 + } + + accepted, _, err := mime.ParseMediaType(r.Header.Get("Accept")) + if err != nil { + http.Error(w, err.Error(), http.StatusNotAcceptable) + return + } + + var retApp moduleLib.App + var retAppContent moduleLib.AppContent + + retApp, err = h.client.GetApp(name, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + retAppContent, err = h.client.GetAppContent(name, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch accepted { + case "multipart/form-data": + mpw := multipart.NewWriter(w) + w.Header().Set("Content-Type", mpw.FormDataContentType()) + w.WriteHeader(http.StatusOK) + pw, err := mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/json"}, "Content-Disposition": {"form-data; name=metadata"}}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := json.NewEncoder(pw).Encode(retApp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + pw, err = mpw.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/octet-stream"}, "Content-Disposition": {"form-data; name=file; filename=fileContent"}}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fcBytes, err := base64.StdEncoding.DecodeString(retAppContent.FileContent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = pw.Write(fcBytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case "application/json": + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(retApp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case "application/octet-stream": + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + fcBytes, err := base64.StdEncoding.DecodeString(retAppContent.FileContent) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(fcBytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + default: + http.Error(w, "set Accept: multipart/form-data, application/json or application/octet-stream", http.StatusMultipleChoices) + return + } +} + +// deleteAppHandler handles DELETE operations on a particular App Name +func (h appHandler) deleteAppHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + projectName := vars["project-name"] + compositeAppName := vars["composite-app-name"] + compositeAppVersion := vars["version"] + name := vars["app-name"] + + err := h.client.DeleteApp(name, projectName, compositeAppName, compositeAppVersion) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/orchestrator/api/composite_app_handler.go b/src/orchestrator/api/composite_app_handler.go index 42c72cdb..b54c488e 100644 --- a/src/orchestrator/api/composite_app_handler.go +++ b/src/orchestrator/api/composite_app_handler.go @@ -35,7 +35,6 @@ type compositeAppHandler struct { } // createHandler handles creation of the CompositeApp entry in the database -// This is a multipart handler func (h compositeAppHandler) createHandler(w http.ResponseWriter, r *http.Request) { var c moduleLib.CompositeApp 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 f0804107..1844fb32 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)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, 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)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, 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)) + resp := executeRequest(request, NewRouter(nil, nil, nil, testCase.controllerClient, 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 1e273349..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)) + 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)) + 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)) + 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 a6c72ae3..f95c057e 100644 --- a/src/orchestrator/cmd/main.go +++ b/src/orchestrator/cmd/main.go @@ -34,7 +34,7 @@ func main() { rand.Seed(time.Now().UnixNano()) - err := db.InitializeDatabaseConnection() + err := db.InitializeDatabaseConnection("mco") if err != nil { log.Println("Unable to initialize database connection...") log.Println(err) @@ -47,7 +47,7 @@ func main() { log.Fatalln("Exiting...") } - httpRouter := api.NewRouter(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/contextdb/contextdb.go b/src/orchestrator/pkg/infra/contextdb/contextdb.go index d18af227..58832a19 100644 --- a/src/orchestrator/pkg/infra/contextdb/contextdb.go +++ b/src/orchestrator/pkg/infra/contextdb/contextdb.go @@ -29,6 +29,8 @@ type ContextDb interface { Put(key string, value interface{}) error // Delete k,v Delete(key string) error + // Delete all keys in heirarchy + DeleteAll(key string) error // Gets Json Struct from db Get(key string, value interface{}) error // Returns all keys with a prefix diff --git a/src/orchestrator/pkg/infra/contextdb/etcd.go b/src/orchestrator/pkg/infra/contextdb/etcd.go index a1922d3b..44f8ab48 100644 --- a/src/orchestrator/pkg/infra/contextdb/etcd.go +++ b/src/orchestrator/pkg/infra/contextdb/etcd.go @@ -156,13 +156,26 @@ func (e *EtcdClient) GetAllKeys(key string) ([]string, error) { return keys, nil } +// DeleteAll keys from Etcd DB +func (e *EtcdClient) DeleteAll(key string) error { + cli := getEtcd(e) + if cli == nil { + return pkgerrors.Errorf("Etcd Client not initialized") + } + _, err := cli.Delete(context.Background(), key, clientv3.WithPrefix()) + if err != nil { + return pkgerrors.Errorf("Delete failed etcd entry: %s", err.Error()) + } + return nil +} + // Delete values from Etcd DB func (e *EtcdClient) Delete(key string) error { cli := getEtcd(e) if cli == nil { return pkgerrors.Errorf("Etcd Client not initialized") } - _, err := cli.Delete(context.Background(), key, clientv3.WithPrefix()) + _, err := cli.Delete(context.Background(), key) if err != nil { return pkgerrors.Errorf("Delete failed etcd entry: %s", err.Error()) } diff --git a/src/orchestrator/pkg/infra/db/README.md b/src/orchestrator/pkg/infra/db/README.md index ff482b98..71da1e0a 100644 --- a/src/orchestrator/pkg/infra/db/README.md +++ b/src/orchestrator/pkg/infra/db/README.md @@ -130,7 +130,7 @@ key := CompositeAppKey{ NOTE: Key structure can be different from the original key and can include Query fields also. ANY operation is not supported for Query fields. -### Remove +### RemoveAll Arguments: ```go @@ -139,6 +139,15 @@ key interface ``` Similar to find. This will remove one or more documents based on the key structure. +### Remove + +Arguments: +```go +collection string +key interface +``` +This will remove one document based on the key structure. If child refrences exist for the key then the document will not be removed. + ### Unmarshal Data in mongo is stored as `bson` which is a compressed form of `json`. We need mongo to convert the stored `bson` data to regular `json` @@ -147,3 +156,4 @@ that we can use in our code when returned. `bson.Unmarshal` API is used to achieve this. + diff --git a/src/orchestrator/pkg/infra/db/mock.go b/src/orchestrator/pkg/infra/db/mock.go index afa4b024..e43be8fb 100644 --- a/src/orchestrator/pkg/infra/db/mock.go +++ b/src/orchestrator/pkg/infra/db/mock.go @@ -15,6 +15,7 @@ package db import ( "encoding/json" + "fmt" pkgerrors "github.com/pkg/errors" ) @@ -44,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 } @@ -62,8 +67,9 @@ func (m *MockDB) Read(table string, key Key, tag string) ([]byte, error) { return nil, m.Err } + str := fmt.Sprintf("%v", key) for k, v := range m.Items { - if k == key.String() { + if k == str { return v[tag], nil } } @@ -71,7 +77,26 @@ 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 } +func (m *MockDB) Remove(table string, key Key) error { + return m.Err +} diff --git a/src/orchestrator/pkg/infra/db/mongo.go b/src/orchestrator/pkg/infra/db/mongo.go index 30eb899f..a344aa1c 100644 --- a/src/orchestrator/pkg/infra/db/mongo.go +++ b/src/orchestrator/pkg/infra/db/mongo.go @@ -49,6 +49,8 @@ type MongoCollection interface { opts ...*options.FindOptions) (*mongo.Cursor, error) UpdateOne(ctx context.Context, filter interface{}, update interface{}, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) + CountDocuments(ctx context.Context, filter interface{}, + opts ...*options.CountOptions) (int64, error) } // MongoStore is an implementation of the db.Store interface @@ -543,8 +545,8 @@ func (m *MongoStore) Find(coll string, key Key, tag string) ([][]byte, error) { return result, nil } -// Remove method to remove the documet by key -func (m *MongoStore) Remove(coll string, key Key) error { +// RemoveAll method to removes all the documet matching key +func (m *MongoStore) RemoveAll(coll string, key Key) error { if !m.validateParams(coll, key) { return pkgerrors.New("Mandatory fields are missing") } @@ -560,3 +562,29 @@ func (m *MongoStore) Remove(coll string, key Key) error { } return nil } + +// Remove method to remove the documet by key if no child references +func (m *MongoStore) Remove(coll string, key Key) error { + if !m.validateParams(coll, key) { + return pkgerrors.New("Mandatory fields are missing") + } + c := getCollection(coll, m) + ctx := context.Background() + filter, err := m.findFilter(key) + if err != nil { + return err + } + count, err := c.CountDocuments(context.Background(), filter) + if err != nil { + return pkgerrors.Errorf("Error finding: %s", err.Error()) + } + if count > 1 { + return pkgerrors.Errorf("Can't delete parent without deleting child references first") + } + _, err = c.DeleteOne(ctx, filter) + if err != nil { + return pkgerrors.Errorf("Error Deleting from database: %s", err.Error()) + } + return nil +} + diff --git a/src/orchestrator/pkg/infra/db/mongo_test.go b/src/orchestrator/pkg/infra/db/mongo_test.go index d506dbda..d57c19dd 100644 --- a/src/orchestrator/pkg/infra/db/mongo_test.go +++ b/src/orchestrator/pkg/infra/db/mongo_test.go @@ -76,7 +76,12 @@ 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 (c *mockCollection) CountDocuments(ctx context.Context, filter interface{}, + opts ...*options.CountOptions) (int64, error) { + return 1, c.Err } func TestCreate(t *testing.T) { @@ -463,4 +468,3 @@ func TestDelete(t *testing.T) { }) } } - diff --git a/src/orchestrator/pkg/infra/db/store.go b/src/orchestrator/pkg/infra/db/store.go index c8a8f744..e87722cd 100644 --- a/src/orchestrator/pkg/infra/db/store.go +++ b/src/orchestrator/pkg/infra/db/store.go @@ -29,7 +29,6 @@ var DBconn Store // that wants to use the Store interface. This allows various // db backends and key types. type Key interface { - String() string } // Store is an interface for accessing the database @@ -60,17 +59,20 @@ type Store interface { // Find the document(s) with key and get the tag values from the document(s) Find(coll string, key Key, tag string) ([][]byte, error) - // Removes the document(s) matching the key + // Removes the document(s) matching the key if no child reference in collection Remove(coll string, key Key) error + + // Remove all the document(s) matching the key + RemoveAll(coll string, key Key) error } // CreateDBClient creates the DB client -func createDBClient(dbType string) error { +func createDBClient(dbType string, dbName string) error { var err error switch dbType { case "mongo": // create a mongodb database with orchestrator as the name - DBconn, err = NewMongoStore("orchestrator", nil) + DBconn, err = NewMongoStore(dbName, nil) default: return pkgerrors.New(dbType + "DB not supported") } @@ -97,8 +99,8 @@ func DeSerialize(str string, v interface{}) error { // InitializeDatabaseConnection sets up the connection to the // configured database to allow the application to talk to it. -func InitializeDatabaseConnection() error { - err := createDBClient(config.GetConfiguration().DatabaseType) +func InitializeDatabaseConnection(dbName string) error { + err := createDBClient(config.GetConfiguration().DatabaseType, dbName) if err != nil { return pkgerrors.Cause(err) } diff --git a/src/orchestrator/pkg/infra/db/store_test.go b/src/orchestrator/pkg/infra/db/store_test.go index 42a41787..fb23e232 100644 --- a/src/orchestrator/pkg/infra/db/store_test.go +++ b/src/orchestrator/pkg/infra/db/store_test.go @@ -23,7 +23,7 @@ func TestCreateDBClient(t *testing.T) { t.Run("Successfully create DB client", func(t *testing.T) { expected := &MongoStore{} - err := createDBClient("mongo") + err := createDBClient("mongo", "testdb") if err != nil { t.Fatalf("CreateDBClient returned an error (%s)", err) } @@ -32,7 +32,7 @@ func TestCreateDBClient(t *testing.T) { } }) t.Run("Fail to create client for unsupported DB", func(t *testing.T) { - err := createDBClient("fakeDB") + err := createDBClient("fakeDB", "testdb2") if err == nil { t.Fatal("CreateDBClient didn't return an error") } diff --git a/src/orchestrator/pkg/infra/validation/validation.go b/src/orchestrator/pkg/infra/validation/validation.go new file mode 100644 index 00000000..d744dc3d --- /dev/null +++ b/src/orchestrator/pkg/infra/validation/validation.go @@ -0,0 +1,264 @@ +/* + * 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 validation + +import ( + "archive/tar" + "compress/gzip" + "io" + "net" + "regexp" + "strings" + + pkgerrors "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/util/validation" +) + +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 +} + +func IsIpv4Cidr(cidr string) error { + _, _, err := net.ParseCIDR(cidr) + if err != nil { + return pkgerrors.Wrapf(err, "could not parse subnet %v", cidr) + } + return nil +} + +func IsIpv4(ip string) error { + addr := net.ParseIP(ip) + if addr == nil { + return pkgerrors.Errorf("invalid ipv4 address %v", ip) + } + return nil +} + +// default name check - matches valid label value with addtion that length > 0 +func IsValidName(name string) []string { + var errs []string + + errs = validation.IsValidLabelValue(name) + if len(name) == 0 { + errs = append(errs, "name must have non-zero length") + } + return errs +} + +const VALID_NAME_STR string = "NAME" + +var validNameRegEx = regexp.MustCompile("^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$") + +const VALID_ALPHA_STR string = "ALPHA" + +var validAlphaStrRegEx = regexp.MustCompile("^[A-Za-z]*$") + +const VALID_ALPHANUM_STR string = "ALPHANUM" + +var validAlphaNumStrRegEx = regexp.MustCompile("^[A-Za-z0-9]*$") + +// doesn't verify valid base64 length - just checks for proper base64 characters +const VALID_BASE64_STR string = "BASE64" + +var validBase64StrRegEx = regexp.MustCompile("^[A-Za-z0-9+/]+={0,2}$") + +const VALID_ANY_STR string = "ANY" + +var validAnyStrRegEx = regexp.MustCompile("(?s)^.*$") + +// string check - validates for conformance to provided lengths and specified content +// min and max - the string +// if format string provided - check against matching predefined +func IsValidString(str string, min, max int, format string) []string { + var errs []string + + if min > max { + errs = append(errs, "Invalid string length constraints - min is greater than max") + return errs + } + + if len(str) < min { + errs = append(errs, "string length is less than the minimum constraint") + return errs + } + if len(str) > max { + errs = append(errs, "string length is greater than the maximum constraint") + return errs + } + + switch format { + case VALID_ALPHA_STR: + if !validAlphaStrRegEx.MatchString(str) { + errs = append(errs, "string does not match the alpha only constraint") + } + case VALID_ALPHANUM_STR: + if !validAlphaNumStrRegEx.MatchString(str) { + errs = append(errs, "string does not match the alphanumeric only constraint") + } + case VALID_NAME_STR: + if !validNameRegEx.MatchString(str) { + errs = append(errs, "string does not match the valid k8s name constraint") + } + case VALID_BASE64_STR: + if !validBase64StrRegEx.MatchString(str) { + errs = append(errs, "string does not match the valid base64 characters constraint") + } + if len(str)%4 != 0 { + errs = append(errs, "base64 string length should be a multiple of 4") + } + case VALID_ANY_STR: + if !validAnyStrRegEx.MatchString(str) { + errs = append(errs, "string does not match the any characters constraint") + } + default: + // invalid string format supplied + errs = append(errs, "an invalid string constraint was supplied") + } + + return errs +} + +// validate that label conforms to kubernetes label conventions +// general label format expected is: +// "<labelprefix>/<labelname>=<Labelvalue>" +// where labelprefix matches DNS1123Subdomain format +// labelname matches DNS1123Label format +// +// Input labels are allowed to match following formats: +// "<DNS1123Subdomain>/<DNS1123Label>=<Labelvalue>" +// "<DNS1123Label>=<LabelValue>" +// "<LabelValue>" +func IsValidLabel(label string) []string { + var labelerrs []string + + expectLabelName := false + expectLabelPrefix := false + + // split label up into prefix, name and value + // format: prefix/name=value + var labelprefix, labelname, labelvalue string + + kv := strings.SplitN(label, "=", 2) + if len(kv) == 1 { + labelprefix = "" + labelname = "" + labelvalue = kv[0] + } else { + pn := strings.SplitN(kv[0], "/", 2) + if len(pn) == 1 { + labelprefix = "" + labelname = pn[0] + } else { + labelprefix = pn[0] + labelname = pn[1] + expectLabelPrefix = true + } + labelvalue = kv[1] + // if "=" was in the label input, then expect a non-zero length name + expectLabelName = true + } + + // check label prefix validity - prefix is optional + if len(labelprefix) > 0 { + errs := validation.IsDNS1123Subdomain(labelprefix) + if len(errs) > 0 { + labelerrs = append(labelerrs, "Invalid label prefix - label=["+label+"%], labelprefix=["+labelprefix+"], errors: ") + for _, err := range errs { + labelerrs = append(labelerrs, err) + } + } + } else if expectLabelPrefix { + labelerrs = append(labelerrs, "Invalid label prefix - label=["+label+"%], labelprefix=["+labelprefix+"]") + } + if expectLabelName { + errs := validation.IsDNS1123Label(labelname) + if len(errs) > 0 { + labelerrs = append(labelerrs, "Invalid label name - label=["+label+"%], labelname=["+labelname+"], errors: ") + for _, err := range errs { + labelerrs = append(labelerrs, err) + } + } + } + if len(labelvalue) > 0 { + errs := validation.IsValidLabelValue(labelvalue) + if len(errs) > 0 { + labelerrs = append(labelerrs, "Invalid label value - label=["+label+"%], labelvalue=["+labelvalue+"], errors: ") + for _, err := range errs { + labelerrs = append(labelerrs, err) + } + } + } else { + // expect a non-zero value + labelerrs = append(labelerrs, "Invalid label value - label=["+label+"%], labelvalue=["+labelvalue+"]") + } + + return labelerrs +} + +func IsValidNumber(value, min, max int) []string { + var errs []string + + if min > max { + errs = append(errs, "invalid constraints") + return errs + } + + if value < min { + errs = append(errs, "value less than minimum") + } + if value > max { + errs = append(errs, "value greater than maximum") + } + return errs +} diff --git a/src/orchestrator/pkg/infra/validation/validation_test.go b/src/orchestrator/pkg/infra/validation/validation_test.go new file mode 100644 index 00000000..5109b6c7 --- /dev/null +++ b/src/orchestrator/pkg/infra/validation/validation_test.go @@ -0,0 +1,466 @@ +/* + * 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 validation + +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") + } + }) +} + +func TestIsValidName(t *testing.T) { + t.Run("Valid Names", func(t *testing.T) { + validnames := []string{ + "abc123", + "1_abc123.ONE", + "0abcABC_-.5", + "123456789012345678901234567890123456789012345678901234567890123", // max of 63 characters + } + for _, name := range validnames { + errs := IsValidName(name) + if len(errs) > 0 { + t.Errorf("Valid name failed to pass: %v", name) + } + } + }) + + t.Run("Invalid Names", func(t *testing.T) { + invalidnames := []string{ + "", // empty + "_abc123", // starts with non-alphanum + "-abc123", // starts with non-alphanum + ".abc123", // starts with non-alphanum + "abc123-", // ends with non-alphanum + "abc123_", // ends with non-alphanum + "abc123.", // ends with non-alphanum + "1_abc-123.O=NE", // contains not allowed character + "1_a/bc-123.ONE", // contains not allowed character + "1234567890123456789012345678901234567890123456789012345678901234", // longer than 63 characters + } + for _, name := range invalidnames { + errs := IsValidName(name) + if len(errs) == 0 { + t.Errorf("Invalid name passed: %v", name) + } + } + }) +} + +func TestIsIpv4Cidr(t *testing.T) { + t.Run("Valid IPv4 Cidr", func(t *testing.T) { + validipv4cidr := []string{ + "1.2.3.4/32", + "10.11.12.0/24", + "192.168.1.2/8", + "255.0.0.0/16", + } + for _, ip := range validipv4cidr { + err := IsIpv4Cidr(ip) + if err != nil { + t.Errorf("Valid IPv4 CIDR string failed to pass: %v", ip) + } + } + }) + + t.Run("Invalid IPv4 Cidr", func(t *testing.T) { + invalidipv4cidr := []string{ + "", + "1.2.3.4.5/32", + "1.2.3.415/16", + "1.2.3.4/33", + "2.3.4/24", + "1.2.3.4", + "1.2.3.4/", + } + for _, ip := range invalidipv4cidr { + err := IsIpv4Cidr(ip) + if err == nil { + t.Errorf("Invalid IPv4 Cidr passed: %v", ip) + } + } + }) +} + +func TestIsIpv4(t *testing.T) { + t.Run("Valid IPv4", func(t *testing.T) { + validipv4 := []string{ + "1.2.3.42", + "10.11.12.0", + "192.168.1.2", + "255.0.0.0", + "255.255.255.255", + "0.0.0.0", + } + for _, ip := range validipv4 { + err := IsIpv4(ip) + if err != nil { + t.Errorf("Valid IPv4 string failed to pass: %v", ip) + } + } + }) + + t.Run("Invalid IPv4", func(t *testing.T) { + invalidipv4 := []string{ + "", + "1.2.3.4.5", + "1.2.3.45/32", + "1.2.3.4a", + "2.3.4", + "1.2.3.400", + "256.255.255.255", + "10,11,12,13", + "1.2.3.4/", + } + for _, ip := range invalidipv4 { + err := IsIpv4(ip) + if err == nil { + t.Errorf("Invalid IPv4 passed: %v", ip) + } + } + }) +} + +func TestIsValidString(t *testing.T) { + t.Run("Valid Strings", func(t *testing.T) { + validStrings := []struct { + str string + min int + max int + format string + }{ + { + str: "abc123", + min: 0, + max: 16, + format: VALID_NAME_STR, + }, + { + str: "ab-c1_2.3", + min: 0, + max: 16, + format: VALID_NAME_STR, + }, + { + str: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + min: 0, + max: 62, + format: VALID_ALPHANUM_STR, + }, + { + str: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + min: 0, + max: 52, + format: VALID_ALPHA_STR, + }, + { + str: "", + min: 0, + max: 52, + format: VALID_ALPHA_STR, + }, + { + str: "", + min: 0, + max: 52, + format: VALID_ALPHANUM_STR, + }, + { + str: "dGhpcyBpcyBhCnRlc3Qgc3RyaW5nCg==", + min: 0, + max: 52, + format: VALID_BASE64_STR, + }, + { + str: "\t\n \n0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=,.<>/?'\"\\[]{}\n", + min: 0, + max: 256, + format: VALID_ANY_STR, + }, + } + for _, test := range validStrings { + errs := IsValidString(test.str, test.min, test.max, test.format) + if len(errs) > 0 { + t.Errorf("Valid string failed to pass: str:%v, min:%v, max:%v, format:%v", test.str, test.min, test.max, test.format) + } + } + }) + + t.Run("Invalid Strings", func(t *testing.T) { + inValidStrings := []struct { + str string + min int + max int + format string + }{ + { + str: "abc123", + min: 0, + max: 5, + format: VALID_NAME_STR, + }, + { + str: "", + min: 0, + max: 5, + format: VALID_NAME_STR, + }, + { + str: "-ab-c1_2.3", + min: 0, + max: 16, + format: VALID_NAME_STR, + }, + { + str: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=", + min: 0, + max: 100, + format: VALID_ALPHANUM_STR, + }, + { + str: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456", + min: 0, + max: 62, + format: VALID_ALPHA_STR, + }, + { + str: "", + min: 1, + max: 52, + format: VALID_ALPHA_STR, + }, + { + str: "abc123", + min: 1, + max: 3, + format: VALID_ALPHA_STR, + }, + { + str: "", + min: 1, + max: 52, + format: VALID_ALPHANUM_STR, + }, + { + str: "dGhpcyBpcyBhCnRlc3Qgc3RyaW5nCg===", + min: 0, + max: 52, + format: VALID_BASE64_STR, + }, + { + str: "dGhpcyBpcyBhCnRlc3=Qgc3RyaW5nCg==", + min: 0, + max: 52, + format: VALID_BASE64_STR, + }, + { + str: "dGhpcyBpcyBhCnRlc3#Qgc3RyaW5nCg==", + min: 0, + max: 52, + format: VALID_BASE64_STR, + }, + { + str: "\t\n \n0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=,.<>/?'\"\\[]{}\n", + min: 0, + max: 10, + format: VALID_ANY_STR, + }, + { + str: "abc123", + min: 0, + max: 10, + format: "unknownformat", + }, + } + for _, test := range inValidStrings { + errs := IsValidString(test.str, test.min, test.max, test.format) + if len(errs) == 0 { + t.Errorf("Invalid string passed: str:%v, min:%v, max:%v, format:%v", test.str, test.min, test.max, test.format) + } + } + }) +} + +func TestIsValidLabel(t *testing.T) { + t.Run("Valid Labels", func(t *testing.T) { + validlabels := []string{ + "kubernetes.io/hostname=localhost", + "hostname=localhost", + "localhost", + } + for _, label := range validlabels { + errs := IsValidLabel(label) + if len(errs) > 0 { + t.Errorf("Valid label failed to pass: %v %v", label, errs) + } + } + }) + + t.Run("Invalid Labels", func(t *testing.T) { + invalidlabels := []string{ + "", + "kubernetes$.io/hostname=localhost", + "hostname==localhost", + "=localhost", + "/hostname=localhost", + ".a.b/hostname=localhost", + "kubernetes.io/hostname", + "kubernetes.io/hostname=", + "kubernetes.io/1234567890123456789012345678901234567890123456789012345678901234=localhost", // too long name + "kubernetes.io/hostname=localhost1234567890123456789012345678901234567890123456789012345678901234", // too long value + "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234/hostname=localhost", // too long prefix + } + for _, label := range invalidlabels { + errs := IsValidLabel(label) + if len(errs) == 0 { + t.Errorf("Invalid label passed: %v", label) + } + } + }) +} + +func TestIsValidNumber(t *testing.T) { + t.Run("Valid Number", func(t *testing.T) { + validNumbers := []struct { + value int + min int + max int + }{ + { + value: 0, + min: 0, + max: 5, + }, + { + value: 1000, + min: 0, + max: 4095, + }, + { + value: 0, + min: 0, + max: 0, + }, + { + value: -100, + min: -200, + max: -99, + }, + { + value: 123, + min: 123, + max: 123, + }, + } + for _, test := range validNumbers { + err := IsValidNumber(test.value, test.min, test.max) + if len(err) > 0 { + t.Errorf("Valid number failed to pass - value:%v, min:%v, max:%v", test.value, test.min, test.max) + } + } + }) + + t.Run("Invalid Number", func(t *testing.T) { + inValidNumbers := []struct { + value int + min int + max int + }{ + { + value: 6, + min: 0, + max: 5, + }, + { + value: 4096, + min: 0, + max: 4095, + }, + { + value: 11, + min: 10, + max: 10, + }, + { + value: -100, + min: -99, + max: -200, + }, + { + value: 123, + min: 223, + max: 123, + }, + } + for _, test := range inValidNumbers { + err := IsValidNumber(test.value, test.min, test.max) + if len(err) == 0 { + t.Errorf("Invalid number passed - value:%v, min:%v, max:%v", test.value, test.min, test.max) + } + } + }) +} diff --git a/src/orchestrator/pkg/module/add_intents.go b/src/orchestrator/pkg/module/add_intents.go new file mode 100644 index 00000000..20fba189 --- /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:"intentname"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"compositeappversion"` + DeploymentIntentGroup string `json:"deploymentintentgroup"` +} + +// 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.go b/src/orchestrator/pkg/module/app.go new file mode 100644 index 00000000..1e1a5974 --- /dev/null +++ b/src/orchestrator/pkg/module/app.go @@ -0,0 +1,231 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governinog permissions and + * limitations under the License. + */ + +package module + +import ( + "encoding/json" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +// App contains metadata for Apps +type App struct { + Metadata AppMetaData `json:"metadata"` +} + +//AppMetaData contains the parameters needed for Apps +type AppMetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `json:"userData1"` + UserData2 string `json:"userData2"` +} + +//AppContent contains fileContent +type AppContent struct { + FileContent string +} + +// AppKey is the key structure that is used in the database +type AppKey struct { + App string `json:"app"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + CompositeAppVersion string `json:"compositeappversion"` +} + +// We will use json marshalling to convert to string to +// preserve the underlying structure. +func (aK AppKey) String() string { + out, err := json.Marshal(aK) + if err != nil { + return "" + } + return string(out) +} + +// AppManager is an interface exposes the App functionality +type AppManager interface { + CreateApp(a App, ac AppContent, p string, cN string, cV string) (App, error) + GetApp(name string, p string, cN string, cV string) (App, error) + GetAppContent(name string, p string, cN string, cV string) (AppContent, error) + GetApps(p string, cN string, cV string) ([]App, error) + DeleteApp(name string, p string, cN string, cV string) error +} + +// AppClient implements the AppManager +// It will also be used to maintain some localized state +type AppClient struct { + storeName string + tagMeta, tagContent string +} + +// NewAppClient returns an instance of the AppClient +// which implements the AppManager +func NewAppClient() *AppClient { + return &AppClient{ + storeName: "orchestrator", + tagMeta: "appmetadata", + tagContent: "appcontent", + } +} + +// CreateApp creates a new collection based on the App +func (v *AppClient) CreateApp(a App, ac AppContent, p string, cN string, cV string) (App, error) { + + //Construct the composite key to select the entry + key := AppKey{ + App: a.Metadata.Name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + + //Check if this App already exists + _, err := v.GetApp(a.Metadata.Name, p, cN, cV) + if err == nil { + return App{}, pkgerrors.New("App already exists") + } + + //Check if Project exists + _, err = NewProjectClient().GetProject(p) + if err != nil { + return App{}, pkgerrors.New("Unable to find the project") + } + + //check if CompositeApp with version exists + _, err = NewCompositeAppClient().GetCompositeApp(cN, cV, p) + if err != nil { + return App{}, pkgerrors.New("Unable to find the composite app with version") + } + + err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, a) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + err = db.DBconn.Insert(v.storeName, key, nil, v.tagContent, ac) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Creating DB Entry") + } + + return a, nil +} + +// GetApp returns the App for corresponding name +func (v *AppClient) GetApp(name string, p string, cN string, cV string) (App, error) { + + //Construct the composite key to select the entry + key := AppKey{ + App: name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + value, err := db.DBconn.Find(v.storeName, key, v.tagMeta) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Get app") + } + + //value is a byte array + if value != nil { + app := App{} + err = db.DBconn.Unmarshal(value[0], &app) + if err != nil { + return App{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return app, nil + } + + return App{}, pkgerrors.New("Error getting app") +} + +// GetAppContent returns content for corresponding app +func (v *AppClient) GetAppContent(name string, p string, cN string, cV string) (AppContent, error) { + + //Construct the composite key to select the entry + key := AppKey{ + App: name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + value, err := db.DBconn.Find(v.storeName, key, v.tagContent) + if err != nil { + return AppContent{}, pkgerrors.Wrap(err, "Get app content") + } + + //value is a byte array + if value != nil { + ac := AppContent{} + err = db.DBconn.Unmarshal(value[0], &ac) + if err != nil { + return AppContent{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + return ac, nil + } + + return AppContent{}, pkgerrors.New("Error getting app content") +} + +// GetApps returns all Apps for given composite App +func (v *AppClient) GetApps(project, compositeApp, compositeAppVersion string) ([]App, error) { + + key := AppKey{ + App: "", + Project: project, + CompositeApp: compositeApp, + CompositeAppVersion: compositeAppVersion, + } + + var resp []App + values, err := db.DBconn.Find(v.storeName, key, v.tagMeta) + if err != nil { + return []App{}, pkgerrors.Wrap(err, "Get Apps") + } + + for _, value := range values { + a := App{} + err = db.DBconn.Unmarshal(value, &a) + if err != nil { + return []App{}, pkgerrors.Wrap(err, "Unmarshaling Value") + } + resp = append(resp, a) + } + + return resp, nil +} + +// DeleteApp deletes the App from database +func (v *AppClient) DeleteApp(name string, p string, cN string, cV string) error { + + //Construct the composite key to select the entry + key := AppKey{ + App: name, + Project: p, + CompositeApp: cN, + CompositeAppVersion: cV, + } + err := db.DBconn.Remove(v.storeName, key) + if err != nil { + return pkgerrors.Wrap(err, "Delete App Entry;") + } + + return nil +} diff --git a/src/orchestrator/pkg/module/app_intent.go b/src/orchestrator/pkg/module/app_intent.go new file mode 100644 index 00000000..a3f4b832 --- /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:"appintent"` + Project string `json:"project"` + CompositeApp string `json:"compositeapp"` + Version string `json:"compositeappversion"` + Intent string `json:"genericplacement"` +} + +// 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: "appintentmetadata", + } +} + +// 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..6cbdf15f --- /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(): { + "genericplacementintentmetadata": []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(): { + "appintentmetadata": []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/app_test.go b/src/orchestrator/pkg/module/app_test.go new file mode 100644 index 00000000..3bbbaf2f --- /dev/null +++ b/src/orchestrator/pkg/module/app_test.go @@ -0,0 +1,327 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package module + +import ( + "reflect" + "strings" + "testing" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + pkgerrors "github.com/pkg/errors" + // pkgerrors "github.com/pkg/errors" +) + +func TestCreateApp(t *testing.T) { + testCases := []struct { + label string + inpApp App + inpAppContent AppContent + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected App + }{ + { + label: "Create App", + inpApp: App{ + Metadata: AppMetaData{ + Name: "testApp", + Description: "A sample app used for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + + inpAppContent: AppContent{ + FileContent: "Sample file content", + }, + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + expected: App{ + Metadata: AppMetaData{ + Name: "testApp", + Description: "A sample app used for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testProject\"," + + "\"Description\": \"Test project for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + }, + CompositeAppKey{CompositeAppName: "testCompositeApp", Version: "v1", Project: "testProject"}.String(): { + "compositeappmetadata": []byte( + "{" + + "\"metadata\":{" + + "\"Name\":\"testCompositeApp\"," + + "\"Description\":\"Test CompositeApp for unit testing\"," + + "\"UserData1\":\"userData1\"," + + "\"UserData2\":\"userData2\"}," + + "\"spec\":{" + + "\"Version\":\"v1\"}" + + "}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + got, err := impl.CreateApp(testCase.inpApp, testCase.inpAppContent, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Create returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Create returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Create returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetApp(t *testing.T) { + + testCases := []struct { + label string + inpApp string + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected App + }{ + { + label: "Get Composite App", + inpApp: "testApp", + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + expected: App{ + Metadata: AppMetaData{ + Name: "testApp", + Description: "Test App for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): { + "appmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testApp\"," + + "\"Description\": \"Test App for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + "appcontent": []byte( + "{" + + "\"FileContent\": \"sample file content\"" + + "}"), + }, + }, + }, + }, + { + label: "Get Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + got, err := impl.GetApp(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Get returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Get returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Get returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetAppContent(t *testing.T) { + + testCases := []struct { + label string + inpApp string + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + expected AppContent + }{ + { + label: "Get App content", + inpApp: "testApp", + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + expected: AppContent{ + FileContent: "Samplefilecontent", + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): { + "appmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testApp\"," + + "\"Description\": \"Test App for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + "appcontent": []byte( + "{" + + "\"FileContent\": \"Samplefilecontent\"" + + "}"), + }, + }, + }, + }, + { + label: "Get Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + got, err := impl.GetAppContent(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Get returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Get returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Get returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestDeleteApp(t *testing.T) { + + testCases := []struct { + label string + inpApp string + inpProject string + inpCompositeAppName string + inpCompositeAppVersion string + expectedError string + mockdb *db.MockDB + }{ + { + label: "Delete App", + inpApp: "testApp", + inpProject: "testProject", + inpCompositeAppName: "testCompositeApp", + inpCompositeAppVersion: "v1", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + AppKey{App: "testApp", Project: "testProject", CompositeApp: "testCompositeApp", CompositeAppVersion: "v1"}.String(): { + "appmetadata": []byte( + "{" + + "\"metadata\": {" + + "\"Name\": \"testApp\"," + + "\"Description\": \"Test App for unit testing\"," + + "\"UserData1\": \"userData1\"," + + "\"UserData2\": \"userData2\"}" + + "}"), + "appcontent": []byte( + "{" + + "\"FileContent\": \"Samplefilecontent\"" + + "}"), + }, + }, + }, + }, + { + label: "Delete Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewAppClient() + err := impl.DeleteApp(testCase.inpApp, testCase.inpProject, testCase.inpCompositeAppName, testCase.inpCompositeAppVersion) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Delete returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Delete returned an unexpected error %s", err) + } + } + }) + } +} 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/compositeapp.go b/src/orchestrator/pkg/module/compositeapp.go index 0a4e158c..70502367 100644 --- a/src/orchestrator/pkg/module/compositeapp.go +++ b/src/orchestrator/pkg/module/compositeapp.go @@ -45,8 +45,8 @@ type CompositeAppSpec struct { // CompositeAppKey is the key structure that is used in the database type CompositeAppKey struct { - CompositeAppName string `json:"compositeappname"` - Version string `json:"version"` + CompositeAppName string `json:"compositeapp"` + Version string `json:"compositeappversion"` Project string `json:"project"` } @@ -70,8 +70,8 @@ type CompositeAppManager interface { // CompositeAppClient implements the CompositeAppManager // It will also be used to maintain some localized state type CompositeAppClient struct { - storeName string - tagMeta, tagContent string + storeName string + tagMeta string } // NewCompositeAppClient returns an instance of the CompositeAppClient @@ -79,7 +79,7 @@ type CompositeAppClient struct { func NewCompositeAppClient() *CompositeAppClient { return &CompositeAppClient{ storeName: "orchestrator", - tagMeta: "compositeAppmetadata", + tagMeta: "compositeappmetadata", } } @@ -105,7 +105,7 @@ func (v *CompositeAppClient) CreateCompositeApp(c CompositeApp, p string) (Compo return CompositeApp{}, pkgerrors.New("Unable to find the project") } - err = db.DBconn.Create(v.storeName, key, v.tagMeta, c) + err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, c) if err != nil { return CompositeApp{}, pkgerrors.Wrap(err, "Creating DB Entry") } @@ -122,7 +122,7 @@ func (v *CompositeAppClient) GetCompositeApp(name string, version string, p stri Version: version, Project: p, } - value, err := db.DBconn.Read(v.storeName, key, v.tagMeta) + value, err := db.DBconn.Find(v.storeName, key, v.tagMeta) if err != nil { return CompositeApp{}, pkgerrors.Wrap(err, "Get composite application") } @@ -130,7 +130,7 @@ func (v *CompositeAppClient) GetCompositeApp(name string, version string, p stri //value is a byte array if value != nil { compApp := CompositeApp{} - err = db.DBconn.Unmarshal(value, &compApp) + err = db.DBconn.Unmarshal(value[0], &compApp) if err != nil { return CompositeApp{}, pkgerrors.Wrap(err, "Unmarshaling Value") } @@ -149,7 +149,7 @@ func (v *CompositeAppClient) DeleteCompositeApp(name string, version string, p s Version: version, Project: p, } - err := db.DBconn.Delete(v.storeName, key, v.tagMeta) + err := db.DBconn.Remove(v.storeName, key) if err != nil { return pkgerrors.Wrap(err, "Delete CompositeApp Entry;") } diff --git a/src/orchestrator/pkg/module/compositeapp_test.go b/src/orchestrator/pkg/module/compositeapp_test.go new file mode 100644 index 00000000..1fc5f5d2 --- /dev/null +++ b/src/orchestrator/pkg/module/compositeapp_test.go @@ -0,0 +1,236 @@ +/* + * Copyright 2020 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package module + +import ( + "reflect" + "strings" + "testing" + + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/db" + + pkgerrors "github.com/pkg/errors" +) + +func TestCreateCompositeApp(t *testing.T) { + testCases := []struct { + label string + inpCompApp CompositeApp + inpProject string + expectedError string + mockdb *db.MockDB + expected CompositeApp + }{ + { + label: "Create Composite App", + inpCompApp: CompositeApp{ + Metadata: CompositeAppMetaData{ + Name: "testCompositeApp", + Description: "A sample composite app used for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: CompositeAppSpec{ + Version: "v1", + }, + }, + + inpProject: "testProject", + expected: CompositeApp{ + Metadata: CompositeAppMetaData{ + Name: "testCompositeApp", + Description: "A sample composite app used for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: CompositeAppSpec{ + Version: "v1", + }, + }, + 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\"}" + + "}"), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewCompositeAppClient() + got, err := impl.CreateCompositeApp(testCase.inpCompApp, testCase.inpProject) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Create returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Create returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Create returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestGetCompositeApp(t *testing.T) { + + testCases := []struct { + label string + inpName string + inpVersion string + inpProject string + expectedError string + mockdb *db.MockDB + expected CompositeApp + }{ + { + label: "Get Composite App", + inpName: "testCompositeApp", + inpVersion: "v1", + inpProject: "testProject", + expected: CompositeApp{ + Metadata: CompositeAppMetaData{ + Name: "testCompositeApp", + Description: "Test CompositeApp for unit testing", + UserData1: "userData1", + UserData2: "userData2", + }, + Spec: CompositeAppSpec{ + Version: "v1", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + CompositeAppKey{CompositeAppName: "testCompositeApp", Version: "v1", Project: "testProject"}.String(): { + "compositeappmetadata": []byte( + "{" + + "\"metadata\":{" + + "\"Name\":\"testCompositeApp\"," + + "\"Description\":\"Test CompositeApp for unit testing\"," + + "\"UserData1\":\"userData1\"," + + "\"UserData2\":\"userData2\"}," + + "\"spec\":{" + + "\"Version\":\"v1\"}" + + "}"), + }, + }, + }, + }, + { + label: "Get Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewCompositeAppClient() + got, err := impl.GetCompositeApp(testCase.inpName, testCase.inpVersion, testCase.inpProject) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Get returned an unexpected error: %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Get returned an unexpected error: %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Get returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + +func TestDeleteCompositeApp(t *testing.T) { + + testCases := []struct { + label string + inpName string + inpVersion string + inpProject string + expectedError string + mockdb *db.MockDB + }{ + { + label: "Delete Composite app", + inpName: "testCompositeApp", + inpVersion: "v1", + inpProject: "testProject", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + CompositeAppKey{CompositeAppName: "testCompositeApp", Version: "v1", Project: "testProject"}.String(): { + "compositeappmetadata": []byte( + "{" + + "\"metadata\":{" + + "\"Name\":\"testCompositeApp\"," + + "\"Description\":\"Test CompositeApp for unit testing\"," + + "\"UserData1\":\"userData1\"," + + "\"UserData2\":\"userData2\"}," + + "\"spec\":{" + + "\"Version\":\"v1\"}" + + "}"), + }, + }, + }, + }, + { + label: "Delete Error", + expectedError: "DB Error", + mockdb: &db.MockDB{ + Err: pkgerrors.New("DB Error"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewCompositeAppClient() + err := impl.DeleteCompositeApp(testCase.inpName, testCase.inpVersion, testCase.inpProject) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Delete returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Delete returned an unexpected error %s", err) + } + } + }) + } +} 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..cfbf53e2 --- /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:"deploymentintentgroup"` + 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 (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: "deploymentintentgroupmetadata", + } +} + +// 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..0fdeb4a1 --- /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(): { + "deploymentintentgroupmetadata": []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..73849474 --- /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:"genericplacement"` + 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 (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: "genericplacementintentmetadata", + } +} + +// 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..d779e81f --- /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(): { + "genericplacementintentmetadata": []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 a94a4207..c697bbff 100644 --- a/src/orchestrator/pkg/module/module.go +++ b/src/orchestrator/pkg/module/module.go @@ -18,9 +18,16 @@ package module // Client for using the services in the orchestrator type Client struct { - Project *ProjectClient - CompositeApp *CompositeAppClient - Controller *ControllerClient + Project *ProjectClient + CompositeApp *CompositeAppClient + App *AppClient + Controller *ControllerClient + GenericPlacementIntent *GenericPlacementIntentClient + AppIntent *AppIntentClient + DeploymentIntentGroup *DeploymentIntentGroupClient + Intent *IntentClient + CompositeProfile *CompositeProfileClient + AppProfile *AppProfileClient // Add Clients for API's here } @@ -29,7 +36,14 @@ func NewClient() *Client { c := &Client{} c.Project = NewProjectClient() c.CompositeApp = NewCompositeAppClient() + c.App = NewAppClient() c.Controller = NewControllerClient() + 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/pkg/module/project.go b/src/orchestrator/pkg/module/project.go index a95251b5..a6f59254 100644 --- a/src/orchestrator/pkg/module/project.go +++ b/src/orchestrator/pkg/module/project.go @@ -90,7 +90,7 @@ func (v *ProjectClient) CreateProject(p Project) (Project, error) { return Project{}, pkgerrors.New("Project already exists") } - err = db.DBconn.Create(v.storeName, key, v.tagMeta, p) + err = db.DBconn.Insert(v.storeName, key, nil, v.tagMeta, p) if err != nil { return Project{}, pkgerrors.Wrap(err, "Creating DB Entry") } @@ -105,7 +105,7 @@ func (v *ProjectClient) GetProject(name string) (Project, error) { key := ProjectKey{ ProjectName: name, } - value, err := db.DBconn.Read(v.storeName, key, v.tagMeta) + value, err := db.DBconn.Find(v.storeName, key, v.tagMeta) if err != nil { return Project{}, pkgerrors.Wrap(err, "Get Project") } @@ -113,7 +113,7 @@ func (v *ProjectClient) GetProject(name string) (Project, error) { //value is a byte array if value != nil { proj := Project{} - err = db.DBconn.Unmarshal(value, &proj) + err = db.DBconn.Unmarshal(value[0], &proj) if err != nil { return Project{}, pkgerrors.Wrap(err, "Unmarshaling Value") } @@ -130,7 +130,7 @@ func (v *ProjectClient) DeleteProject(name string) error { key := ProjectKey{ ProjectName: name, } - err := db.DBconn.Delete(v.storeName, key, v.tagMeta) + err := db.DBconn.Remove(v.storeName, key) if err != nil { return pkgerrors.Wrap(err, "Delete Project Entry;") } diff --git a/src/orchestrator/pkg/rtcontext/rtcontext.go b/src/orchestrator/pkg/rtcontext/rtcontext.go new file mode 100644 index 00000000..e1f1c03b --- /dev/null +++ b/src/orchestrator/pkg/rtcontext/rtcontext.go @@ -0,0 +1,238 @@ +/* + * 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 rtcontext + +import ( + "fmt" + "math/rand" + "time" + "strings" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/contextdb" + pkgerrors "github.com/pkg/errors" +) + +const maxrand = 0x7fffffffffffffff +const prefix string = "/context/" + +type RunTimeContext struct { + cid interface{} +} + +type Rtcontext interface { + RtcCreate() (interface{}, error) + RtcGet() (interface{}, error) + RtcAddLevel(handle interface{}, level string, value string) (interface{}, error) + RtcAddResource(handle interface{}, resname string, value interface{}) (interface{}, error) + RtcAddInstruction(handle interface{}, level string, insttype string, value interface{}) (interface{}, error) + RtcDeletePair(handle interface{}) (error) + RtcDeletePrefix(handle interface{}) (error) + RtcGetHandles(handle interface{}) ([]interface{}, error) + RtcGetValue(handle interface{}, value interface{}) (error) + RtcUpdateValue(handle interface{}, value interface{}) (error) +} + +//Create context by assiging a new id +func (rtc *RunTimeContext) RtcCreate() (interface{}, error) { + + ra := rand.New(rand.NewSource(time.Now().UnixNano())) + rn := ra.Int63n(maxrand) + id := fmt.Sprintf("%v", rn) + cid := (prefix + id + "/") + rtc.cid = interface{}(cid) + + err := contextdb.Db.Put(cid, id) + if err != nil { + return nil, pkgerrors.Errorf("Error creating run time context: %s", err.Error()) + } + + return rtc.cid, nil +} + +//Get the root handle +func (rtc *RunTimeContext) RtcGet() (interface{}, error) { + str := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, prefix) { + return nil, pkgerrors.Errorf("Not a valid run time context") + } + + var value string + err := contextdb.Db.Get(str, &value) + if err != nil { + return nil, pkgerrors.Errorf("Error getting run time context metadata: %s", err.Error()) + } + if !strings.Contains(str, value) { + return nil, pkgerrors.Errorf("Error matching run time context metadata") + } + + return rtc.cid, nil +} + +//Add a new level at a given handle and return the new handle +func (rtc *RunTimeContext) RtcAddLevel(handle interface{}, level string, value string) (interface{}, error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return nil, pkgerrors.Errorf("Not a valid run time context handle") + } + + if level == "" { + return nil, pkgerrors.Errorf("Not a valid run time context level") + } + if value == "" { + return nil, pkgerrors.Errorf("Not a valid run time context level value") + } + + key := str + level + "/" + value + "/" + err := contextdb.Db.Put(key, value) + if err != nil { + return nil, pkgerrors.Errorf("Error adding run time context level: %s", err.Error()) + } + + return (interface{})(key), nil +} + +// Add a resource under the given level and return new handle +func (rtc *RunTimeContext) RtcAddResource(handle interface{}, resname string, value interface{}) (interface{}, error) { + + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return nil, pkgerrors.Errorf("Not a valid run time context handle") + } + if resname == "" { + return nil, pkgerrors.Errorf("Not a valid run time context resource name") + } + if value == nil { + return nil, pkgerrors.Errorf("Not a valid run time context resource value") + } + + k := str + "resource" + "/" + resname + "/" + err := contextdb.Db.Put(k, value) + if err != nil { + return nil, pkgerrors.Errorf("Error adding run time context resource: %s", err.Error()) + } + return (interface{})(k), nil +} + +// Add instruction at a given level and type, return the new handle +func (rtc *RunTimeContext) RtcAddInstruction(handle interface{}, level string, insttype string, value interface{}) (interface{}, error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return nil, pkgerrors.Errorf("Not a valid run time context handle") + } + + if level == "" { + return nil, pkgerrors.Errorf("Not a valid run time context level") + } + if insttype == "" { + return nil, pkgerrors.Errorf("Not a valid run time context instruction type") + } + if value == nil { + return nil, pkgerrors.Errorf("Not a valid run time context instruction value") + } + + k := str + level + "/" + "instruction" + "/" + insttype +"/" + err := contextdb.Db.Put(k, fmt.Sprintf("%v", value)) + if err != nil { + return nil, pkgerrors.Errorf("Error adding run time context instruction: %s", err.Error()) + } + + return (interface{})(k), nil +} + +//Delete the key value pair using given handle +func (rtc *RunTimeContext) RtcDeletePair(handle interface{}) (error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return pkgerrors.Errorf("Not a valid run time context handle") + } + + err := contextdb.Db.Delete(str) + if err != nil { + return pkgerrors.Errorf("Error deleting run time context pair: %s", err.Error()) + } + + return nil +} + +// Delete all handles underneath the given handle +func (rtc *RunTimeContext) RtcDeletePrefix(handle interface{}) (error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return pkgerrors.Errorf("Not a valid run time context handle") + } + + err := contextdb.Db.DeleteAll(str) + if err != nil { + return pkgerrors.Errorf("Error deleting run time context with prefix: %s", err.Error()) + } + + return nil +} + +// Return the list of handles under the given handle +func (rtc *RunTimeContext) RtcGetHandles(handle interface{}) ([]interface{}, error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return nil, pkgerrors.Errorf("Not a valid run time context handle") + } + + s, err := contextdb.Db.GetAllKeys(str) + if err != nil { + return nil, pkgerrors.Errorf("Error getting run time context handles: %s", err.Error()) + } + r := make([]interface{}, len(s)) + for i, v := range s { + r[i] = v + } + return r, nil +} + +// Get the value for a given handle +func (rtc *RunTimeContext) RtcGetValue(handle interface{}, value interface{}) (error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return pkgerrors.Errorf("Not a valid run time context handle") + } + + err := contextdb.Db.Get(str, value) + if err != nil { + return pkgerrors.Errorf("Error getting run time context value: %s", err.Error()) + } + + return nil +} + +// Update the value of a given handle +func (rtc *RunTimeContext) RtcUpdateValue(handle interface{}, value interface{}) (error) { + str := fmt.Sprintf("%v", handle) + sid := fmt.Sprintf("%v", rtc.cid) + if !strings.HasPrefix(str, sid) { + return pkgerrors.Errorf("Not a valid run time context handle") + } + err := contextdb.Db.Put(str, value) + if err != nil { + return pkgerrors.Errorf("Error updating run time context value: %s", err.Error()) + } + return nil + +} diff --git a/src/orchestrator/pkg/rtcontext/rtcontext_test.go b/src/orchestrator/pkg/rtcontext/rtcontext_test.go new file mode 100644 index 00000000..29df9a25 --- /dev/null +++ b/src/orchestrator/pkg/rtcontext/rtcontext_test.go @@ -0,0 +1,596 @@ +/* + * 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 rtcontext + +import ( + "testing" + "strings" + "github.com/onap/multicloud-k8s/src/orchestrator/pkg/infra/contextdb" + pkgerrors "github.com/pkg/errors" +) + +// MockContextDb for mocking contextdb +type MockContextDb struct { + + Items map[string]interface{} + Err error +} + +// Put function +func (c *MockContextDb) Put(key string, val interface{}) (error) { + if c.Items == nil { + c.Items = make(map[string]interface{}) + } + c.Items[key] = val + return c.Err +} + +// Get function +func (c *MockContextDb) Get(key string, val interface{}) (error) { + var s *string + s = val.(*string) + for kvKey, kvValue := range c.Items { + if kvKey == key { + *s = kvValue.(string) + return c.Err + } + } + return c.Err +} + +// Delete function +func (c *MockContextDb) Delete(key string) (error) { + delete(c.Items, key) + return c.Err +} + +// Delete all function +func (c *MockContextDb) DeleteAll(key string) (error) { + for kvKey, _ := range c.Items { + delete(c.Items, kvKey) + } + return c.Err +} + +// GetAllKeys function +func (c *MockContextDb) GetAllKeys(path string) ([]string, error) { + var keys []string + + for k, _ := range c.Items { + keys = append(keys, string(k)) + } + return keys, c.Err +} + +func (c *MockContextDb) HealthCheck() error { + return nil +} + +func TestRtcCreate(t *testing.T) { + var rtc = RunTimeContext{} + testCases := []struct { + label string + mockContextDb *MockContextDb + expectedError string + }{ + { + label: "Success case", + mockContextDb: &MockContextDb{}, + }, + { + label: "Create returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Client not intialized")}, + expectedError: "Error creating run time context:", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + _, err := rtc.RtcCreate() + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + + }) + } +} + +func TestRtcGet(t *testing.T) { + var rtc = RunTimeContext{} + var rtc1 = RunTimeContext{"/context/5345674458787728/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + expectedError string + }{ + { + label: "Success case", + mockContextDb: &MockContextDb{}, + }, + { + label: "Get returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Client not intialized")}, + expectedError: "Error getting run time context metadata:", + }, + { + label: "Context handle does not match", + mockContextDb: &MockContextDb{Err: nil}, + expectedError: "Error matching run time context metadata", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + switch testCase.label { + case "Success case": + contextdb.Db = testCase.mockContextDb + chandle, err := rtc.RtcCreate() + if err != nil { + t.Fatalf("Create returned an error (%s)", err) + } + ghandle, err := rtc.RtcGet() + if err != nil { + t.Fatalf("Get returned an error (%s)", err) + } + if ( chandle != ghandle ) { + t.Fatalf("Create and Get does not match") + } + case "Get returns error case": + contextdb.Db = testCase.mockContextDb + _, err := rtc.RtcGet() + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + case "Context handle does not match": + contextdb.Db = testCase.mockContextDb + contextdb.Db.Put("/context/5345674458787728/", "6345674458787728") + _, err := rtc1.RtcGet() + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + } + }) + } +} + +func TestRtcAddLevel(t *testing.T) { + var rtc = RunTimeContext{"/context/3528435435454354/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + handle interface{} + level string + value string + expectedError string + }{ + { + label: "Success case", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/", + level: "app", + value: "testapp1", + }, + { + label: "Not a valid rtc handle", + mockContextDb: &MockContextDb{}, + handle: "/context/9528435435454354/", + level: "app", + value: "testapp1", + expectedError: "Not a valid run time context handle", + }, + { + label: "Not a valid rtc level", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/", + level: "", + value: "testapp1", + expectedError: "Not a valid run time context level", + }, + { + label: "Not a valid rtc value", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/", + level: "app", + value: "", + expectedError: "Not a valid run time context level value", + }, + { + label: "Put returns error", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Client not intialized")}, + handle: "/context/3528435435454354/", + level: "app", + value: "testapp1", + expectedError: "Error adding run time context level:", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + _, err := rtc.RtcAddLevel(testCase.handle, testCase.level, testCase.value) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcAddResource(t *testing.T) { + var rtc = RunTimeContext{"/context/3528435435454354/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + handle interface{} + resname string + value interface{} + expectedError string + }{ + { + label: "Success case", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + resname: "R1", + value: "res1", + }, + { + label: "Not a valid rtc handle", + mockContextDb: &MockContextDb{}, + handle: "/context/9528435435454354/app/apptest1/cluster/cluster1/", + resname: "R1", + value: "res1", + expectedError: "Not a valid run time context handle", + }, + { + label: "Not a valid rtc resource name", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + resname: "", + value: "res1", + expectedError: "Not a valid run time context resource name", + }, + { + label: "Not a valid rtc value", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + resname: "R1", + value: nil, + expectedError: "Not a valid run time context resource value", + }, + { + label: "Put returns error", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Client not intialized")}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + resname: "R1", + value: "res1", + expectedError: "Error adding run time context resource:", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + _, err := rtc.RtcAddResource(testCase.handle, testCase.resname, testCase.value) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcAddInstruction(t *testing.T) { + var rtc = RunTimeContext{"/context/3528435435454354/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + handle interface{} + level string + insttype string + value interface{} + expectedError string + }{ + { + label: "Success case", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + level: "resource", + insttype: "order", + value: "{resorder: [R3, R1, R2]}", + }, + { + label: "Not a valid rtc handle", + mockContextDb: &MockContextDb{}, + handle: "/context/9528435435454354/app/apptest1/cluster/cluster1/", + level: "resource", + insttype: "order", + value: "{resorder: [R3, R1, R2]}", + expectedError: "Not a valid run time context handle", + }, + { + label: "Not a valid rtc level name", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + level: "", + insttype: "order", + value: "{resorder: [R3, R1, R2]}", + expectedError: "Not a valid run time context level", + }, + { + label: "Not a valid rtc instruction type", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + level: "resource", + insttype: "", + value: "{resorder: [R3, R1, R2]}", + expectedError: "Not a valid run time context instruction type", + }, + { + label: "Not a valid rtc value", + mockContextDb: &MockContextDb{}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + level: "resource", + insttype: "order", + value: nil, + expectedError: "Not a valid run time context instruction value", + }, + { + label: "Put returns error", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Client not intialized")}, + handle: "/context/3528435435454354/app/apptest1/cluster/cluster1/", + level: "resource", + insttype: "order", + value: "{resorder: [R3, R1, R2]}", + expectedError: "Error adding run time context instruction:", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + _, err := rtc.RtcAddInstruction(testCase.handle, testCase.level, testCase.insttype, testCase.value) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcGetHandles(t *testing.T) { + var rtc = RunTimeContext{"/context/5345674458787728/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + key interface{} + expectedError string + }{ + { + label: "Not valid input handle case", + mockContextDb: &MockContextDb{}, + key: "/context/3528435435454354/", + expectedError: "Not a valid run time context handle", + }, + { + label: "Contextdb call returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Key does not exist")}, + key: "/context/5345674458787728/", + expectedError: "Error getting run time context handles:", + }, + { + label: "Success case", + mockContextDb: &MockContextDb{}, + key: "/context/5345674458787728/", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + if testCase.label == "Success case" { + contextdb.Db.Put("/context/5345674458787728/", 5345674458787728) + } + _, err := rtc.RtcGetHandles(testCase.key) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcGetValue(t *testing.T) { + var rtc = RunTimeContext{"/context/5345674458787728/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + key interface{} + expectedError string + }{ + { + label: "Not valid input handle case", + mockContextDb: &MockContextDb{}, + key: "/context/3528435435454354/", + expectedError: "Not a valid run time context handle", + }, + { + label: "Contextdb call returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Key does not exist")}, + key: "/context/5345674458787728/", + expectedError: "Error getting run time context value:", + }, + { + label: "Success case", + mockContextDb: &MockContextDb{}, + key: "/context/5345674458787728/", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + if testCase.label == "Success case" { + contextdb.Db.Put("/context/5345674458787728/", "5345674458787728") + } + var val string + err := rtc.RtcGetValue(testCase.key, &val) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcUpdateValue(t *testing.T) { + var rtc = RunTimeContext{"/context/5345674458787728/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + key interface{} + value interface{} + expectedError string + }{ + { + label: "Not valid input handle case", + mockContextDb: &MockContextDb{}, + key: "/context/3528435435454354/", + value: "{apporder: [app1, app2, app3]}", + expectedError: "Not a valid run time context handle", + }, + { + label: "Contextdb call returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Key does not exist")}, + key: "/context/5345674458787728/", + value: "{apporder: [app1, app2, app3]}", + expectedError: "Error updating run time context value:", + }, + { + label: "Success case", + mockContextDb: &MockContextDb{}, + key: "/context/5345674458787728/", + value: "{apporder: [app2, app3, app1]}", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + if testCase.label == "Success case" { + contextdb.Db.Put("/context/5345674458787728/", "5345674458787728") + } + err := rtc.RtcUpdateValue(testCase.key, testCase.value) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcDeletePair(t *testing.T) { + var rtc = RunTimeContext{"/context/5345674458787728/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + key interface{} + expectedError string + }{ + { + label: "Not valid input handle case", + mockContextDb: &MockContextDb{}, + key: "/context/3528435435454354/", + expectedError: "Not a valid run time context handle", + }, + { + label: "Contextdb call returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Key does not exist")}, + key: "/context/5345674458787728/", + expectedError: "Error deleting run time context pair:", + }, + { + label: "Success case", + mockContextDb: &MockContextDb{}, + key: "/context/5345674458787728/", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + err := rtc.RtcDeletePair(testCase.key) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} + +func TestRtcDeletePrefix(t *testing.T) { + var rtc = RunTimeContext{"/context/5345674458787728/"} + testCases := []struct { + label string + mockContextDb *MockContextDb + key interface{} + expectedError string + }{ + { + label: "Not valid input handle case", + mockContextDb: &MockContextDb{}, + key: "/context/3528435435454354/", + expectedError: "Not a valid run time context handle", + }, + { + label: "Contextdb call returns error case", + mockContextDb: &MockContextDb{Err: pkgerrors.Errorf("Key does not exist")}, + key: "/context/5345674458787728/", + expectedError: "Error deleting run time context with prefix:", + }, + { + label: "Success case", + mockContextDb: &MockContextDb{}, + key: "/context/5345674458787728/", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + contextdb.Db = testCase.mockContextDb + err := rtc.RtcDeletePrefix(testCase.key) + if err != nil { + if !strings.Contains(string(err.Error()), testCase.expectedError) { + t.Fatalf("Method returned an error (%s)", err) + } + } + }) + } +} |