From a1373742a2c3f980360e4980f3b23b0ff3480ae6 Mon Sep 17 00:00:00 2001 From: Shashank Kumar Shankar Date: Mon, 20 Aug 2018 15:50:50 -0700 Subject: [PATCH] Seed code for k8s multicloud plugin This patch provides the initial seed code for the multicloud Kubernetes plugin and also provides the plugin feature to add new Kubernetes kinds. Change-Id: Ie5ee414656665070cde2834c4855ac2ebc179a9a Issue-ID: MULTICLOUD-301 Signed-off-by: Shashank Kumar Shankar Signed-off-by: Victor Morales --- .gitignore | 23 ++ .gitreview | 4 + README.md | 41 +++ deployments/Dockerfile | 21 ++ deployments/build.sh | 32 ++ deployments/docker-compose.yml | 41 +++ doc/create_vl.png | Bin 0 -> 36106 bytes doc/create_vnf.png | Bin 0 -> 47090 bytes doc/sampleCommands.rst | 71 ++++ doc/swagger.yaml | 184 ++++++++++ src/k8splugin/Gopkg.lock | 353 +++++++++++++++++++ src/k8splugin/Gopkg.toml | 46 +++ src/k8splugin/Makefile | 43 +++ src/k8splugin/api/api.go | 120 +++++++ src/k8splugin/api/handler.go | 377 +++++++++++++++++++++ src/k8splugin/api/handler_test.go | 316 +++++++++++++++++ src/k8splugin/api/model.go | 76 +++++ src/k8splugin/cmd/main.go | 64 ++++ src/k8splugin/csar/parser.go | 207 +++++++++++ src/k8splugin/csar/parser_test.go | 130 +++++++ src/k8splugin/db/DB.go | 42 +++ src/k8splugin/db/consul.go | 112 ++++++ src/k8splugin/db/db_test.go | 40 +++ src/k8splugin/krd/krd.go | 44 +++ src/k8splugin/krd/krd_test.go | 34 ++ src/k8splugin/krd/plugins.go | 44 +++ src/k8splugin/mock_files/mock_configs/mock_config | 29 ++ .../mock_files/mock_plugins/mockplugin.go | 43 +++ .../mock_files/mock_yamls/deployment.yaml | 24 ++ src/k8splugin/mock_files/mock_yamls/metadata.yaml | 16 + src/k8splugin/mock_files/mock_yamls/service.yaml | 21 ++ src/k8splugin/plugins/deployment/plugin.go | 136 ++++++++ src/k8splugin/plugins/namespace/plugin.go | 68 ++++ src/k8splugin/plugins/service/plugin.go | 131 +++++++ 34 files changed, 2933 insertions(+) create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 README.md create mode 100644 deployments/Dockerfile create mode 100755 deployments/build.sh create mode 100644 deployments/docker-compose.yml create mode 100644 doc/create_vl.png create mode 100644 doc/create_vnf.png create mode 100644 doc/sampleCommands.rst create mode 100644 doc/swagger.yaml create mode 100644 src/k8splugin/Gopkg.lock create mode 100644 src/k8splugin/Gopkg.toml create mode 100644 src/k8splugin/Makefile create mode 100644 src/k8splugin/api/api.go create mode 100644 src/k8splugin/api/handler.go create mode 100644 src/k8splugin/api/handler_test.go create mode 100644 src/k8splugin/api/model.go create mode 100644 src/k8splugin/cmd/main.go create mode 100644 src/k8splugin/csar/parser.go create mode 100644 src/k8splugin/csar/parser_test.go create mode 100644 src/k8splugin/db/DB.go create mode 100644 src/k8splugin/db/consul.go create mode 100644 src/k8splugin/db/db_test.go create mode 100644 src/k8splugin/krd/krd.go create mode 100644 src/k8splugin/krd/krd_test.go create mode 100644 src/k8splugin/krd/plugins.go create mode 100644 src/k8splugin/mock_files/mock_configs/mock_config create mode 100644 src/k8splugin/mock_files/mock_plugins/mockplugin.go create mode 100644 src/k8splugin/mock_files/mock_yamls/deployment.yaml create mode 100644 src/k8splugin/mock_files/mock_yamls/metadata.yaml create mode 100644 src/k8splugin/mock_files/mock_yamls/service.yaml create mode 100644 src/k8splugin/plugins/deployment/plugin.go create mode 100644 src/k8splugin/plugins/namespace/plugin.go create mode 100644 src/k8splugin/plugins/service/plugin.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0b583fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# IDE +.DS_Store +.vscode +*-workspace + +# Directories +pkg +bin +target +src/github.com +src/golang.org +src/k8splugin/vendor +src/k8splugin/kubeconfig/* +deployments/k8plugin + +# Binaries +*.so +src/k8splugin/csar/mock_plugins/*.so +src/k8splugin/plugins/**/*.so + +# Tests +*.test +*.out diff --git a/.gitreview b/.gitreview new file mode 100644 index 00000000..aab7df3c --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=gerrit.onap.org +port=29418 +project=multicloud/k8s.git diff --git a/README.md b/README.md new file mode 100644 index 00000000..0e62378f --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ + + +# MultiCloud-k8-plugin + +MultiCloud Kubernetes plugin for ONAP multicloud. + +# Installation + +Requirements: +* Go 1.10 +* Dep + +Steps: + +* Clone repo in GOPATH src: + * `cd $GOPATH/src && git clone https://git.onap.org/multicloud/k8s` + +* Run unit tests: + * `make build` + +* Compile to build Binary: + * `make deploy` + +# Archietecture + +Create Virtual Network Function + +![Create VNF](./doc/create_vnf.png) + +Create Virtual Link + +![Create VL](./doc/create_vl.png) diff --git a/deployments/Dockerfile b/deployments/Dockerfile new file mode 100644 index 00000000..407af509 --- /dev/null +++ b/deployments/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:jessie + +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 + +ENV CSAR_DIR "/opt/csar" +ENV KUBE_CONFIG_DIR "/opt/kubeconfig" +ENV DATABASE_TYPE "consul" +ENV DATABASE_IP "127.0.0.1" + +EXPOSE 8081 + +WORKDIR /opt/multicloud/k8s +ADD ./k8plugin ./ +ADD ./*.so ./ + +CMD ["./k8plugin"] diff --git a/deployments/build.sh b/deployments/build.sh new file mode 100755 index 00000000..667be5f5 --- /dev/null +++ b/deployments/build.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2018 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 +############################################################################## + +set -o nounset +set -o pipefail +set -o xtrace + +function generate_binary { + GOPATH=$(go env GOPATH) + rm -f k8plugin + rm -f *.so + $GOPATH/bin/dep ensure -v + for plugin in deployment namespace service; do + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildmode=plugin -a -tags netgo -o ./$plugin.so ../src/k8splugin/plugins/$plugin/plugin.go + done + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -tags netgo -o ./k8plugin ../src/k8splugin/cmd/main.go +} + +function build_image { + echo "Start build docker image." + docker-compose build --no-cache +} + +generate_binary +build_image diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml new file mode 100644 index 00000000..0d347b13 --- /dev/null +++ b/deployments/docker-compose.yml @@ -0,0 +1,41 @@ +# Copyright 2018 Intel Corporation. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# 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. + +version: '3' + +services: + multicloud-k8s: + image: nexus3.onap.org:10003/onap/multicloud/k8plugin + build: + context: ./ + args: + - HTTP_PROXY=$HTTP_PROXY + - HTTPS_PROXY=$HTTPS_PROXY + ports: + - "8081:8081" + environment: + - CSAR_DIR=/opt/csar + - KUBE_CONFIG_DIR=/opt/kubeconfig + - DATABASE_TYPE=consul + - DATABASE_IP=consul-svr + depends_on: + - "consul" + volumes: + - /opt/csar:/opt/csar + - /opt/kubeconfig:/opt/kubeconfig + consul: + image: consul + hostname: consul-svr + environment: + - CONSUL_LOCAL_CONFIG={"datacenter":"us_west","server":true} + command: ["agent", "-server", "-bootstrap-expect=1"] + volumes: + - /opt/consul/config:/consul/config diff --git a/doc/create_vl.png b/doc/create_vl.png new file mode 100644 index 0000000000000000000000000000000000000000..803b6da8d1edd2f88a74761fbe85f848beceecc8 GIT binary patch literal 36106 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 literal 0 HcmV?d00001 diff --git a/doc/create_vnf.png b/doc/create_vnf.png new file mode 100644 index 0000000000000000000000000000000000000000..27b50d7912931c5a0ef14cc65295f38302793361 GIT binary patch literal 47090 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 mHYm7C$~BvuZjsCnKc,"service":<>}" + out, err := json.Marshal(resourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Create VNF deployment error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + serializedResourceNameMap := string(out) + + // key: cloud1-default-uuid + // value: "{"deployment":<>,"service":<>}" + err = db.DBconn.CreateEntry(internalVNFID, serializedResourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Create VNF deployment error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + resp := CreateVnfResponse{ + VNFID: externalVNFID, + CloudRegionID: resource.CloudRegionID, + Namespace: resource.Namespace, + VNFComponents: resourceNameMap, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(resp) +} + +// ListHandler the existing VNF instances created in a given Kubernetes cluster +func ListHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + cloudRegionID := vars["cloudRegionID"] + namespace := vars["namespace"] + prefix := cloudRegionID + "-" + namespace + + internalVNFIDs, err := db.DBconn.ReadAll(prefix) + if err != nil { + werr := pkgerrors.Wrap(err, "Get VNF list error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + if len(internalVNFIDs) == 0 { + w.WriteHeader(http.StatusNotFound) + return + } + + // TODO: There is an edge case where if namespace is passed but is missing some characters + // trailing, it will print the result with those excluding characters. This is because of + // the way I am trimming the Prefix. This fix is needed. + + var editedList []string + + for _, id := range internalVNFIDs { + if len(id) > 0 { + editedList = append(editedList, strings.TrimPrefix(id, prefix)[1:]) + } + } + + if len(editedList) == 0 { + editedList = append(editedList, "") + } + + resp := ListVnfsResponse{ + VNFs: editedList, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +// DeleteHandler method terminates an individual VNF instance. +func DeleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + cloudRegionID := vars["cloudRegionID"] // cloud1 + namespace := vars["namespace"] // default + externalVNFID := vars["externalVNFID"] // uuid + + // cloud1-default-uuid + internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID + + // (TODO): Read kubeconfig for specific Cloud Region from local file system + // if present or download it from AAI + // err := DownloadKubeConfigFromAAI(resource.CloudRegionID, os.Getenv("KUBE_CONFIG_DIR") + kubeclient, err := GetVNFClient(os.Getenv("KUBE_CONFIG_DIR") + "/" + cloudRegionID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // key: cloud1-default-uuid + // value: "{"deployment":<>,"service":<>}" + serializedResourceNameMap, found, err := db.DBconn.ReadEntry(internalVNFID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if found == false { + w.WriteHeader(http.StatusNotFound) + return + } + + /* + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + */ + deserializedResourceNameMap := make(map[string][]string) + err = json.Unmarshal([]byte(serializedResourceNameMap), &deserializedResourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Delete VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + err = csar.DestroyVNF(deserializedResourceNameMap, namespace, &kubeclient) + if err != nil { + werr := pkgerrors.Wrap(err, "Delete VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + err = db.DBconn.DeleteEntry(internalVNFID) + if err != nil { + werr := pkgerrors.Wrap(err, "Delete VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) +} + +// // UpdateHandler method to update a VNF instance. +// func UpdateHandler(w http.ResponseWriter, r *http.Request) { +// vars := mux.Vars(r) +// id := vars["vnfInstanceId"] + +// var resource UpdateVnfRequest + +// if r.Body == nil { +// http.Error(w, "Body empty", http.StatusBadRequest) +// return +// } + +// err := json.NewDecoder(r.Body).Decode(&resource) +// if err != nil { +// http.Error(w, err.Error(), http.StatusUnprocessableEntity) +// return +// } + +// err = validateBody(resource) +// if err != nil { +// http.Error(w, err.Error(), http.StatusUnprocessableEntity) +// return +// } + +// kubeData, err := utils.ReadCSARFromFileSystem(resource.CsarID) + +// if kubeData.Deployment == nil { +// werr := pkgerrors.Wrap(err, "Update VNF deployment error") +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// return +// } +// kubeData.Deployment.SetUID(types.UID(id)) + +// if err != nil { +// werr := pkgerrors.Wrap(err, "Update VNF deployment information error") +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// return +// } + +// // (TODO): Read kubeconfig for specific Cloud Region from local file system +// // if present or download it from AAI +// s, err := NewVNFInstanceService("../kubeconfig/config") +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } + +// err = s.Client.UpdateDeployment(kubeData.Deployment, resource.Namespace) +// if err != nil { +// werr := pkgerrors.Wrap(err, "Update VNF error") + +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// return +// } + +// resp := UpdateVnfResponse{ +// DeploymentID: id, +// } + +// w.Header().Set("Content-Type", "application/json") +// w.WriteHeader(http.StatusCreated) + +// err = json.NewEncoder(w).Encode(resp) +// if err != nil { +// werr := pkgerrors.Wrap(err, "Parsing output of new VNF error") +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// } +// } + +// GetHandler retrieves information about a VNF instance by reading an individual VNF instance resource. +func GetHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + cloudRegionID := vars["cloudRegionID"] // cloud1 + namespace := vars["namespace"] // default + externalVNFID := vars["externalVNFID"] // uuid + + // cloud1-default-uuid + internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID + + // key: cloud1-default-uuid + // value: "{"deployment":<>,"service":<>}" + serializedResourceNameMap, found, err := db.DBconn.ReadEntry(internalVNFID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if found == false { + w.WriteHeader(http.StatusNotFound) + return + } + + /* + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + */ + deserializedResourceNameMap := make(map[string][]string) + err = json.Unmarshal([]byte(serializedResourceNameMap), &deserializedResourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Get VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + resp := GetVnfResponse{ + VNFID: externalVNFID, + CloudRegionID: cloudRegionID, + Namespace: namespace, + VNFComponents: deserializedResourceNameMap, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} diff --git a/src/k8splugin/api/handler_test.go b/src/k8splugin/api/handler_test.go new file mode 100644 index 00000000..df573d94 --- /dev/null +++ b/src/k8splugin/api/handler_test.go @@ -0,0 +1,316 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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" + "k8s.io/client-go/kubernetes" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "k8splugin/csar" + "k8splugin/db" +) + +type mockDB struct { + db.DatabaseConnection +} + +func (c *mockDB) InitializeDatabase() error { + return nil +} + +func (c *mockDB) CheckDatabase() error { + return nil +} + +func (c *mockDB) CreateEntry(key string, value string) error { + return nil +} + +func (c *mockDB) ReadEntry(key string) (string, bool, error) { + str := "{\"deployment\":[\"cloud1-default-uuid-sisedeploy\"],\"service\":[\"cloud1-default-uuid-sisesvc\"]}" + return str, true, nil +} + +func (c *mockDB) DeleteEntry(key string) error { + return nil +} + +func (c *mockDB) ReadAll(key string) ([]string, error) { + returnVal := []string{"cloud1-default-uuid1", "cloud1-default-uuid2"} + return returnVal, nil +} + +func executeRequest(req *http.Request) *httptest.ResponseRecorder { + router := NewRouter("") + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + return recorder +} + +func checkResponseCode(t *testing.T, expected, actual int) { + if expected != actual { + t.Errorf("Expected response code %d. Got %d\n", expected, actual) + } +} + +func TestVNFInstanceCreation(t *testing.T) { + t.Run("Succesful create a VNF", func(t *testing.T) { + payload := []byte(`{ + "cloud_region_id": "region1", + "namespace": "test", + "csar_id": "UUID-1", + "oof_parameters": [{ + "key1": "value1", + "key2": "value2", + "key3": {} + }], + "network_parameters": { + "oam_ip_address": { + "connection_point": "string", + "ip_address": "string", + "workload_name": "string" + } + } + }`) + + data := map[string][]string{ + "deployment": []string{"cloud1-default-uuid-sisedeploy"}, + "service": []string{"cloud1-default-uuid-sisesvc"}, + } + + expected := &CreateVnfResponse{ + VNFID: "test_UUID", + CloudRegionID: "region1", + Namespace: "test", + VNFComponents: data, + } + + var result CreateVnfResponse + + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", bytes.NewBuffer(payload)) + + GetVNFClient = func(configPath string) (kubernetes.Clientset, error) { + return kubernetes.Clientset{}, nil + } + + csar.CreateVNF = func(id string, r string, n string, kubeclient *kubernetes.Clientset) (string, map[string][]string, error) { + return "externaluuid", data, nil + } + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusCreated, response.Code) + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstanceCreation returned:\n result=%v\n expected=%v", err, expected.VNFComponents) + } + }) + t.Run("Missing body failure", func(t *testing.T) { + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", nil) + response := executeRequest(req) + + checkResponseCode(t, http.StatusBadRequest, response.Code) + }) + t.Run("Invalid JSON request format", func(t *testing.T) { + payload := []byte("invalid") + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", bytes.NewBuffer(payload)) + response := executeRequest(req) + checkResponseCode(t, http.StatusUnprocessableEntity, response.Code) + }) + t.Run("Missing parameter failure", func(t *testing.T) { + payload := []byte(`{ + "csar_id": "testID", + "oof_parameters": { + "key_values": { + "key1": "value1", + "key2": "value2" + } + }, + "vnf_instance_name": "test", + "vnf_instance_description": "vRouter_test_description" + }`) + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", bytes.NewBuffer(payload)) + response := executeRequest(req) + checkResponseCode(t, http.StatusUnprocessableEntity, response.Code) + }) +} + +func TestVNFInstancesRetrieval(t *testing.T) { + t.Run("Succesful get a list of VNF", func(t *testing.T) { + expected := &ListVnfsResponse{ + VNFs: []string{"uuid1", "uuid2"}, + } + var result ListVnfsResponse + + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloud1/default", nil) + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstancesRetrieval returned:\n result=%v\n expected=list", err) + } + if !reflect.DeepEqual(*expected, result) { + t.Fatalf("TestVNFInstancesRetrieval returned:\n result=%v\n expected=%v", result, *expected) + } + }) + t.Run("Get empty list", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloudregion1/testnamespace", nil) + db.DBconn = &mockDB{} + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + }) +} + +func TestVNFInstanceDeletion(t *testing.T) { + t.Run("Succesful delete a VNF", func(t *testing.T) { + req, _ := http.NewRequest("DELETE", "/v1/vnf_instances/cloudregion1/testnamespace/1", nil) + + GetVNFClient = func(configPath string) (kubernetes.Clientset, error) { + return kubernetes.Clientset{}, nil + } + + csar.DestroyVNF = func(d map[string][]string, n string, kubeclient *kubernetes.Clientset) error { + return nil + } + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusAccepted, response.Code) + + if result := response.Body.String(); result != "" { + t.Fatalf("TestVNFInstanceDeletion returned:\n result=%v\n expected=%v", result, "") + } + }) + // t.Run("Malformed delete request", func(t *testing.T) { + // req, _ := http.NewRequest("DELETE", "/v1/vnf_instances/foo", nil) + // response := executeRqequest(req) + // checkResponseCode(t, http.StatusBadRequest, response.Code) + // }) +} + +// TODO: Update this test when the UpdateVNF endpoint is fixed. +/* +func TestVNFInstanceUpdate(t *testing.T) { + t.Run("Succesful update a VNF", func(t *testing.T) { + payload := []byte(`{ + "cloud_region_id": "region1", + "csar_id": "UUID-1", + "oof_parameters": [{ + "key1": "value1", + "key2": "value2", + "key3": {} + }], + "network_parameters": { + "oam_ip_address": { + "connection_point": "string", + "ip_address": "string", + "workload_name": "string" + } + } + }`) + expected := &UpdateVnfResponse{ + DeploymentID: "1", + } + + var result UpdateVnfResponse + + req, _ := http.NewRequest("PUT", "/v1/vnf_instances/1", bytes.NewBuffer(payload)) + + GetVNFClient = func(configPath string) (krd.VNFInstanceClientInterface, error) { + return &mockClient{ + update: func() error { + return nil + }, + }, nil + } + utils.ReadCSARFromFileSystem = func(csarID string) (*krd.KubernetesData, error) { + kubeData := &krd.KubernetesData{ + Deployment: &appsV1.Deployment{}, + Service: &coreV1.Service{}, + } + return kubeData, nil + } + + response := executeRequest(req) + checkResponseCode(t, http.StatusCreated, response.Code) + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstanceUpdate returned:\n result=%v\n expected=%v", err, expected.DeploymentID) + } + + if result.DeploymentID != expected.DeploymentID { + t.Fatalf("TestVNFInstanceUpdate returned:\n result=%v\n expected=%v", result.DeploymentID, expected.DeploymentID) + } + }) +} +*/ + +func TestVNFInstanceRetrieval(t *testing.T) { + t.Run("Succesful get a VNF", func(t *testing.T) { + + data := map[string][]string{ + "deployment": []string{"cloud1-default-uuid-sisedeploy"}, + "service": []string{"cloud1-default-uuid-sisesvc"}, + } + + expected := GetVnfResponse{ + VNFID: "1", + CloudRegionID: "cloud1", + Namespace: "default", + VNFComponents: data, + } + + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloud1/default/1", nil) + + GetVNFClient = func(configPath string) (kubernetes.Clientset, error) { + return kubernetes.Clientset{}, nil + } + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + + var result GetVnfResponse + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstanceRetrieval returned:\n result=%v\n expected=%v", err, expected) + } + + if !reflect.DeepEqual(expected, result) { + t.Fatalf("TestVNFInstanceRetrieval returned:\n result=%v\n expected=%v", result, expected) + } + }) + t.Run("VNF not found", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloudregion1/testnamespace/1", nil) + response := executeRequest(req) + + checkResponseCode(t, http.StatusOK, response.Code) + }) +} diff --git a/src/k8splugin/api/model.go b/src/k8splugin/api/model.go new file mode 100644 index 00000000..0e4863c4 --- /dev/null +++ b/src/k8splugin/api/model.go @@ -0,0 +1,76 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 + +// CreateVnfRequest contains the VNF creation request parameters +type CreateVnfRequest struct { + CloudRegionID string `json:"cloud_region_id"` + CsarID string `json:"csar_id"` + OOFParams []map[string]interface{} `json:"oof_parameters"` + NetworkParams NetworkParameters `json:"network_parameters"` + Namespace string `json:"namespace"` + Name string `json:"vnf_instance_name"` + Description string `json:"vnf_instance_description"` +} + +// CreateVnfResponse contains the VNF creation response parameters +type CreateVnfResponse struct { + VNFID string `json:"vnf_id"` + CloudRegionID string `json:"cloud_region_id"` + Namespace string `json:"namespace"` + VNFComponents map[string][]string `json:"vnf_components"` +} + +// ListVnfsResponse contains the list of VNFs response parameters +type ListVnfsResponse struct { + VNFs []string `json:"vnf_id_list"` +} + +// NetworkParameters contains the networking info required by the VNF instance +type NetworkParameters struct { + OAMI OAMIPParams `json:"oam_ip_address"` + // Add other network parameters if necessary. +} + +// OAMIPParams contains the management networking info required by the VNF instance +type OAMIPParams struct { + ConnectionPoint string `json:"connection_point"` + IPAddress string `json:"ip_address"` + WorkLoadName string `json:"workload_name"` +} + +// UpdateVnfRequest contains the VNF creation parameters +type UpdateVnfRequest struct { + CloudRegionID string `json:"cloud_region_id"` + CsarID string `json:"csar_id"` + OOFParams []map[string]interface{} `json:"oof_parameters"` + NetworkParams NetworkParameters `json:"network_parameters"` + Namespace string `json:"namespace"` + Name string `json:"vnf_instance_name"` + Description string `json:"vnf_instance_description"` +} + +// UpdateVnfResponse contains the VNF update response parameters +type UpdateVnfResponse struct { + DeploymentID string `json:"vnf_id"` + Name string `json:"name"` +} + +// GetVnfResponse returns information about a specific VNF instance +type GetVnfResponse struct { + VNFID string `json:"vnf_id"` + CloudRegionID string `json:"cloud_region_id"` + Namespace string `json:"namespace"` + VNFComponents map[string][]string `json:"vnf_components"` +} diff --git a/src/k8splugin/cmd/main.go b/src/k8splugin/cmd/main.go new file mode 100644 index 00000000..ee676549 --- /dev/null +++ b/src/k8splugin/cmd/main.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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" + "flag" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + + "github.com/gorilla/handlers" + "k8s.io/client-go/util/homedir" + + "k8splugin/api" +) + +func main() { + var kubeconfig string + + home := homedir.HomeDir() + if home != "" { + kubeconfig = *flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } + flag.Parse() + + err := api.CheckInitialSettings() + if err != nil { + log.Fatal(err) + } + + httpRouter := api.NewRouter(kubeconfig) + loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) + log.Println("Starting Kubernetes Multicloud API") + + httpServer := &http.Server{ + Handler: loggedRouter, + Addr: ":8081", // Remove hardcoded port number + } + + connectionsClose := make(chan struct{}) + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + httpServer.Shutdown(context.Background()) + close(connectionsClose) + }() + + log.Fatal(httpServer.ListenAndServe()) +} diff --git a/src/k8splugin/csar/parser.go b/src/k8splugin/csar/parser.go new file mode 100644 index 00000000..abd6ad92 --- /dev/null +++ b/src/k8splugin/csar/parser.go @@ -0,0 +1,207 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 csar + +import ( + "encoding/hex" + "io/ioutil" + "log" + "math/rand" + "os" + + "k8s.io/client-go/kubernetes" + + pkgerrors "github.com/pkg/errors" + "gopkg.in/yaml.v2" + + "k8splugin/krd" +) + +func generateExternalVNFID(charLen int) string { + b := make([]byte, charLen/2) + rand.Read(b) + return hex.EncodeToString(b) +} + +// CreateVNF reads the CSAR files from the files system and creates them one by one +var CreateVNF = func(csarID string, cloudRegionID string, namespace string, kubeclient *kubernetes.Clientset) (string, map[string][]string, error) { + namespacePlugin, ok := krd.LoadedPlugins["namespace"] + if !ok { + return "", nil, pkgerrors.New("No plugin for namespace resource found") + } + + symGetNamespaceFunc, err := namespacePlugin.Lookup("GetResource") + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error fetching namespace plugin") + } + + present, err := symGetNamespaceFunc.(func(string, *kubernetes.Clientset) (bool, error))( + namespace, kubeclient) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error in plugin namespace plugin") + } + + if present == false { + symGetNamespaceFunc, err := namespacePlugin.Lookup("CreateResource") + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error fetching namespace plugin") + } + + err = symGetNamespaceFunc.(func(string, *kubernetes.Clientset) error)( + namespace, kubeclient) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error creating "+namespace+" namespace") + } + } + + var path string + + // uuid + externalVNFID := generateExternalVNFID(8) + + // cloud1-default-uuid + internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID + + csarDirPath := os.Getenv("CSAR_DIR") + "/" + csarID + metadataYAMLPath := csarDirPath + "/metadata.yaml" + + seqFile, err := ReadMetadataFile(metadataYAMLPath) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error while reading Metadata File: "+metadataYAMLPath) + } + + resourceYAMLNameMap := make(map[string][]string) + + for _, resource := range seqFile.ResourceTypePathMap { + for resourceName, resourceFileNames := range resource { + // Load/Use Deployment data/client + + var resourceNameList []string + + for _, filename := range resourceFileNames { + path = csarDirPath + "/" + filename + + _, err = os.Stat(path) + if os.IsNotExist(err) { + return "", nil, pkgerrors.New("File " + path + "does not exists") + } + + log.Println("Processing file: " + path) + + genericKubeData := &krd.GenericKubeResourceData{ + YamlFilePath: path, + Namespace: namespace, + InternalVNFID: internalVNFID, + } + + typePlugin, ok := krd.LoadedPlugins[resourceName] + if !ok { + return "", nil, pkgerrors.New("No plugin for resource " + resourceName + " found") + } + + symCreateResourceFunc, err := typePlugin.Lookup("CreateResource") + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error fetching "+resourceName+" plugin") + } + + // cloud1-default-uuid-sisedeploy + internalResourceName, err := symCreateResourceFunc.(func(*krd.GenericKubeResourceData, *kubernetes.Clientset) (string, error))( + genericKubeData, kubeclient) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error in plugin "+resourceName+" plugin") + } + + // ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + resourceNameList = append(resourceNameList, internalResourceName) + + /* + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + } + */ + resourceYAMLNameMap[resourceName] = resourceNameList + } + } + } + + /* + uuid, + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + nil + */ + return externalVNFID, resourceYAMLNameMap, nil +} + +// DestroyVNF deletes VNFs based on data passed +var DestroyVNF = func(data map[string][]string, namespace string, kubeclient *kubernetes.Clientset) error { + /* data: + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + */ + + for resourceName, resourceList := range data { + typePlugin, ok := krd.LoadedPlugins[resourceName] + if !ok { + return pkgerrors.New("No plugin for resource " + resourceName + " found") + } + + symDeleteResourceFunc, err := typePlugin.Lookup("DeleteResource") + if err != nil { + return pkgerrors.Wrap(err, "Error fetching "+resourceName+" plugin") + } + + for _, resourceName := range resourceList { + + log.Println("Deleting resource: " + resourceName) + + err = symDeleteResourceFunc.(func(string, string, *kubernetes.Clientset) error)( + resourceName, namespace, kubeclient) + if err != nil { + return pkgerrors.Wrap(err, "Error destroying "+resourceName) + } + } + } + + return nil +} + +// MetadataFile stores the metadata of execution +type MetadataFile struct { + ResourceTypePathMap []map[string][]string `yaml:"resources"` +} + +// ReadMetadataFile reads the metadata yaml to return the order or reads +var ReadMetadataFile = func(yamlFilePath string) (MetadataFile, error) { + var seqFile MetadataFile + + if _, err := os.Stat(yamlFilePath); err == nil { + log.Println("Reading metadata YAML: " + yamlFilePath) + rawBytes, err := ioutil.ReadFile(yamlFilePath) + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error") + } + + err = yaml.Unmarshal(rawBytes, &seqFile) + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error") + } + } + + return seqFile, nil +} diff --git a/src/k8splugin/csar/parser_test.go b/src/k8splugin/csar/parser_test.go new file mode 100644 index 00000000..cec5395e --- /dev/null +++ b/src/k8splugin/csar/parser_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 csar + +import ( + "io/ioutil" + "k8s.io/client-go/kubernetes" + "log" + "os" + "plugin" + "testing" + + pkgerrors "github.com/pkg/errors" + "gopkg.in/yaml.v2" + + "k8splugin/krd" +) + +func LoadMockPlugins(krdLoadedPlugins *map[string]*plugin.Plugin) error { + if _, err := os.Stat("../mock_files/mock_plugins/mockplugin.so"); os.IsNotExist(err) { + return pkgerrors.New("mockplugin.so does not exist. Please compile mockplugin.go to generate") + } + + mockPlugin, err := plugin.Open("../mock_files/mock_plugins/mockplugin.so") + if err != nil { + return pkgerrors.Cause(err) + } + + (*krdLoadedPlugins)["namespace"] = mockPlugin + (*krdLoadedPlugins)["deployment"] = mockPlugin + (*krdLoadedPlugins)["service"] = mockPlugin + + return nil +} + +func TestCreateVNF(t *testing.T) { + oldkrdPluginData := krd.LoadedPlugins + oldReadMetadataFile := ReadMetadataFile + + defer func() { + krd.LoadedPlugins = oldkrdPluginData + ReadMetadataFile = oldReadMetadataFile + }() + + err := LoadMockPlugins(&krd.LoadedPlugins) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + + ReadMetadataFile = func(yamlFilePath string) (MetadataFile, error) { + var seqFile MetadataFile + + if _, err := os.Stat(yamlFilePath); err == nil { + rawBytes, err := ioutil.ReadFile("../mock_files/mock_yamls/metadata.yaml") + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error") + } + + err = yaml.Unmarshal(rawBytes, &seqFile) + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file unmarshall error") + } + } + + return seqFile, nil + } + + kubeclient := kubernetes.Clientset{} + + t.Run("Successfully create VNF", func(t *testing.T) { + externaluuid, data, err := CreateVNF("uuid", "cloudregion1", "test", &kubeclient) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + + log.Println(externaluuid) + + if data == nil { + t.Fatalf("TestCreateVNF returned empty data (%s)", data) + } + }) + +} + +func TestDeleteVNF(t *testing.T) { + oldkrdPluginData := krd.LoadedPlugins + + defer func() { + krd.LoadedPlugins = oldkrdPluginData + }() + + err := LoadMockPlugins(&krd.LoadedPlugins) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + + kubeclient := kubernetes.Clientset{} + + t.Run("Successfully delete VNF", func(t *testing.T) { + data := map[string][]string{ + "deployment": []string{"cloud1-default-uuid-sisedeploy"}, + "service": []string{"cloud1-default-uuid-sisesvc"}, + } + + err := DestroyVNF(data, "test", &kubeclient) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + }) +} + +func TestReadMetadataFile(t *testing.T) { + t.Run("Successfully read Metadata YAML file", func(t *testing.T) { + _, err := ReadMetadataFile("../mock_files//mock_yamls/metadata.yaml") + if err != nil { + t.Fatalf("TestReadMetadataFile returned an error (%s)", err) + } + }) +} diff --git a/src/k8splugin/db/DB.go b/src/k8splugin/db/DB.go new file mode 100644 index 00000000..c8895088 --- /dev/null +++ b/src/k8splugin/db/DB.go @@ -0,0 +1,42 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 db + +import ( + pkgerrors "github.com/pkg/errors" +) + +// DBconn interface used to talk a concrete Database connection +var DBconn DatabaseConnection + +// DatabaseConnection is an interface for accessing a database +type DatabaseConnection interface { + InitializeDatabase() error + CheckDatabase() error + CreateEntry(string, string) error + ReadEntry(string) (string, bool, error) + DeleteEntry(string) error + ReadAll(string) ([]string, error) +} + +// CreateDBClient creates the DB client +var CreateDBClient = func(dbType string) error { + switch dbType { + case "consul": + DBconn = &ConsulDB{} + return nil + default: + return pkgerrors.New(dbType + "DB not supported") + } +} diff --git a/src/k8splugin/db/consul.go b/src/k8splugin/db/consul.go new file mode 100644 index 00000000..9ab0d826 --- /dev/null +++ b/src/k8splugin/db/consul.go @@ -0,0 +1,112 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 db + +import ( + consulapi "github.com/hashicorp/consul/api" + pkgerrors "github.com/pkg/errors" + "os" +) + +// ConsulDB is an implementation of the DatabaseConnection interface +type ConsulDB struct { + consulClient *consulapi.Client +} + +// InitializeDatabase initialized the initial steps +func (c *ConsulDB) InitializeDatabase() error { + if os.Getenv("DATABASE_IP") == "" { + return pkgerrors.New("DATABASE_IP environment variable not set.") + } + config := consulapi.DefaultConfig() + config.Address = os.Getenv("DATABASE_IP") + ":8500" + + client, err := consulapi.NewClient(config) + if err != nil { + return err + } + c.consulClient = client + return nil +} + +// CheckDatabase checks if the database is running +func (c *ConsulDB) CheckDatabase() error { + kv := c.consulClient.KV() + _, _, err := kv.Get("test", nil) + if err != nil { + return pkgerrors.New("[ERROR] Cannot talk to Datastore. Check if it is running/reachable.") + } + return nil +} + +// CreateEntry is used to create a DB entry +func (c *ConsulDB) CreateEntry(key string, value string) error { + kv := c.consulClient.KV() + + p := &consulapi.KVPair{Key: key, Value: []byte(value)} + + _, err := kv.Put(p, nil) + + if err != nil { + return err + } + + return nil +} + +// ReadEntry returns the internalID for a particular externalID is present in a namespace +func (c *ConsulDB) ReadEntry(key string) (string, bool, error) { + + kv := c.consulClient.KV() + + pair, _, err := kv.Get(key, nil) + + if pair == nil { + return string("No value found for ID: " + key), false, err + } + return string(pair.Value), true, err +} + +// DeleteEntry is used to delete an ID +func (c *ConsulDB) DeleteEntry(key string) error { + + kv := c.consulClient.KV() + + _, err := kv.Delete(key, nil) + + if err != nil { + return err + } + + return nil +} + +// ReadAll is used to get all ExternalIDs in a namespace +func (c *ConsulDB) ReadAll(prefix string) ([]string, error) { + kv := c.consulClient.KV() + + pairs, _, err := kv.List(prefix, nil) + + if len(pairs) == 0 { + return []string{""}, err + } + + var res []string + + for _, keypair := range pairs { + res = append(res, keypair.Key) + } + + return res, err +} diff --git a/src/k8splugin/db/db_test.go b/src/k8splugin/db/db_test.go new file mode 100644 index 00000000..7ad252f5 --- /dev/null +++ b/src/k8splugin/db/db_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 db + +import ( + "reflect" + "testing" +) + +func TestCreateDBClient(t *testing.T) { + oldDBconn := DBconn + + defer func() { + DBconn = oldDBconn + }() + + t.Run("Successfully create DB client", func(t *testing.T) { + expectedDB := ConsulDB{} + + err := CreateDBClient("consul") + if err != nil { + t.Fatalf("TestCreateDBClient returned an error (%s)", err) + } + + if !reflect.DeepEqual(DBconn, &expectedDB) { + t.Fatalf("TestCreateDBClient set DBconn as:\n result=%v\n expected=%v", DBconn, expectedDB) + } + }) +} diff --git a/src/k8splugin/krd/krd.go b/src/k8splugin/krd/krd.go new file mode 100644 index 00000000..2d06e104 --- /dev/null +++ b/src/k8splugin/krd/krd.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 krd + +import ( + "errors" + + pkgerrors "github.com/pkg/errors" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +// GetKubeClient loads the Kubernetes configuation values stored into the local configuration file +var GetKubeClient = func(configPath string) (kubernetes.Clientset, error) { + var clientset *kubernetes.Clientset + + if configPath == "" { + return *clientset, errors.New("config not passed and is not found in ~/.kube. ") + } + + config, err := clientcmd.BuildConfigFromFlags("", configPath) + if err != nil { + return kubernetes.Clientset{}, pkgerrors.Wrap(err, "setConfig: Build config from flags raised an error") + } + + clientset, err = kubernetes.NewForConfig(config) + if err != nil { + return *clientset, err + } + + return *clientset, nil +} diff --git a/src/k8splugin/krd/krd_test.go b/src/k8splugin/krd/krd_test.go new file mode 100644 index 00000000..7047a74c --- /dev/null +++ b/src/k8splugin/krd/krd_test.go @@ -0,0 +1,34 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 krd + +import ( + "reflect" + "testing" +) + +func TestGetKubeClient(t *testing.T) { + t.Run("Successfully create Kube Client", func(t *testing.T) { + + clientset, err := GetKubeClient("../mock_files/mock_configs/mock_config") + if err != nil { + t.Fatalf("TestGetKubeClient returned an error (%s)", err) + } + + if reflect.TypeOf(clientset).Name() != "Clientset" { + t.Fatalf("TestGetKubeClient returned :\n result=%v\n expected=%v", clientset, "Clientset") + } + + }) +} diff --git a/src/k8splugin/krd/plugins.go b/src/k8splugin/krd/plugins.go new file mode 100644 index 00000000..612e3f6b --- /dev/null +++ b/src/k8splugin/krd/plugins.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 krd + +import ( + "plugin" + + appsV1 "k8s.io/api/apps/v1" + coreV1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" +) + +// LoadedPlugins stores references to the stored plugins +var LoadedPlugins = map[string]*plugin.Plugin{} + +// KubeResourceClient has the signature methods to create Kubernetes reources +type KubeResourceClient interface { + CreateResource(GenericKubeResourceData, *kubernetes.Clientset) (string, error) + ListResources(string, string) (*[]string, error) + DeleteResource(string, string, *kubernetes.Clientset) error + GetResource(string, string, *kubernetes.Clientset) (string, error) +} + +// GenericKubeResourceData stores all supported Kubernetes plugin types +type GenericKubeResourceData struct { + YamlFilePath string + Namespace string + InternalVNFID string + + // Add additional Kubernetes plugins below kinds + DeploymentData *appsV1.Deployment + ServiceData *coreV1.Service +} diff --git a/src/k8splugin/mock_files/mock_configs/mock_config b/src/k8splugin/mock_files/mock_configs/mock_config new file mode 100644 index 00000000..9b86ff15 --- /dev/null +++ b/src/k8splugin/mock_files/mock_configs/mock_config @@ -0,0 +1,29 @@ +# Copyright 2018 Intel Corporation. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# 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. + +apiVersion: v1 +kind: Config +clusters: +- name: local + cluster: + insecure-skip-tls-verify: true + server: https://192.168.43.66:6443 +contexts: +- context: + cluster: local + user: admin + name: kubelet-context +current-context: kubelet-context +users: +- name: admin + user: + password: admin + username: admin diff --git a/src/k8splugin/mock_files/mock_plugins/mockplugin.go b/src/k8splugin/mock_files/mock_plugins/mockplugin.go new file mode 100644 index 00000000..9ceec342 --- /dev/null +++ b/src/k8splugin/mock_files/mock_plugins/mockplugin.go @@ -0,0 +1,43 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 ( + "k8s.io/client-go/kubernetes" + + "k8splugin/krd" +) + +func main() {} + +// CreateResource object in a specific Kubernetes resource +func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { + return "externalUUID", nil +} + +// ListResources of existing resources +func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { + returnVal := []string{"cloud1-default-uuid1", "cloud1-default-uuid2"} + return &returnVal, nil +} + +// DeleteResource existing resources +func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { + return nil +} + +// GetResource existing resource host +func GetResource(namespace string, client *kubernetes.Clientset) (bool, error) { + return true, nil +} diff --git a/src/k8splugin/mock_files/mock_yamls/deployment.yaml b/src/k8splugin/mock_files/mock_yamls/deployment.yaml new file mode 100644 index 00000000..eff2fc5a --- /dev/null +++ b/src/k8splugin/mock_files/mock_yamls/deployment.yaml @@ -0,0 +1,24 @@ +# Copyright 2018 Intel Corporation. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sise-deploy +spec: + template: + metadata: + labels: + app: sise + spec: + containers: + - name: sise + image: mhausenblas/simpleservice:0.5.0 \ No newline at end of file diff --git a/src/k8splugin/mock_files/mock_yamls/metadata.yaml b/src/k8splugin/mock_files/mock_yamls/metadata.yaml new file mode 100644 index 00000000..dcc1c32e --- /dev/null +++ b/src/k8splugin/mock_files/mock_yamls/metadata.yaml @@ -0,0 +1,16 @@ +# Copyright 2018 Intel Corporation. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# 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. + +resources: + - deployment: + - deployment.yaml + - service: + - service.yaml diff --git a/src/k8splugin/mock_files/mock_yamls/service.yaml b/src/k8splugin/mock_files/mock_yamls/service.yaml new file mode 100644 index 00000000..297ab1b7 --- /dev/null +++ b/src/k8splugin/mock_files/mock_yamls/service.yaml @@ -0,0 +1,21 @@ +# Copyright 2018 Intel Corporation. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# 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. + +apiVersion: v1 +kind: Service +metadata: + name: sise-svc +spec: + ports: + - port: 80 + protocol: TCP + selector: + app: sise \ No newline at end of file diff --git a/src/k8splugin/plugins/deployment/plugin.go b/src/k8splugin/plugins/deployment/plugin.go new file mode 100644 index 00000000..2b4c7cb7 --- /dev/null +++ b/src/k8splugin/plugins/deployment/plugin.go @@ -0,0 +1,136 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 ( + "io/ioutil" + "log" + "os" + + "k8s.io/client-go/kubernetes" + + pkgerrors "github.com/pkg/errors" + + appsV1 "k8s.io/api/apps/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + + "k8splugin/krd" +) + +// CreateResource object in a specific Kubernetes Deployment +func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { + if kubedata.Namespace == "" { + kubedata.Namespace = "default" + } + + if _, err := os.Stat(kubedata.YamlFilePath); err != nil { + return "", pkgerrors.New("File " + kubedata.YamlFilePath + " not found") + } + + log.Println("Reading deployment YAML") + rawBytes, err := ioutil.ReadFile(kubedata.YamlFilePath) + if err != nil { + return "", pkgerrors.Wrap(err, "Deployment YAML file read error") + } + + log.Println("Decoding deployment YAML") + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode(rawBytes, nil, nil) + if err != nil { + return "", pkgerrors.Wrap(err, "Deserialize deployment error") + } + + switch o := obj.(type) { + case *appsV1.Deployment: + kubedata.DeploymentData = o + default: + return "", pkgerrors.New(kubedata.YamlFilePath + " contains another resource different than Deployment") + } + + kubedata.DeploymentData.Namespace = kubedata.Namespace + kubedata.DeploymentData.Name = kubedata.InternalVNFID + "-" + kubedata.DeploymentData.Name + + result, err := kubeclient.AppsV1().Deployments(kubedata.Namespace).Create(kubedata.DeploymentData) + if err != nil { + return "", pkgerrors.Wrap(err, "Create Deployment error") + } + + return result.GetObjectMeta().GetName(), nil +} + +// ListResources of existing deployments hosted in a specific Kubernetes Deployment +func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { + if namespace == "" { + namespace = "default" + } + + opts := metaV1.ListOptions{ + Limit: limit, + } + opts.APIVersion = "apps/v1" + opts.Kind = "Deployment" + + list, err := kubeclient.AppsV1().Deployments(namespace).List(opts) + if err != nil { + return nil, pkgerrors.Wrap(err, "Get Deployment list error") + } + + result := make([]string, 0, limit) + if list != nil { + for _, deployment := range list.Items { + result = append(result, deployment.Name) + } + } + + return &result, nil +} + +// DeleteResource existing deployments hosting in a specific Kubernetes Deployment +func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { + if namespace == "" { + namespace = "default" + } + + log.Println("Deleting deployment: " + name) + + deletePolicy := metaV1.DeletePropagationForeground + err := kubeclient.AppsV1().Deployments(namespace).Delete(name, &metaV1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + + if err != nil { + return pkgerrors.Wrap(err, "Delete Deployment error") + } + + return nil +} + +// GetResource existing deployment hosting in a specific Kubernetes Deployment +func GetResource(name string, namespace string, kubeclient *kubernetes.Clientset) (string, error) { + if namespace == "" { + namespace = "default" + } + + opts := metaV1.GetOptions{} + opts.APIVersion = "apps/v1" + opts.Kind = "Deployment" + + deployment, err := kubeclient.AppsV1().Deployments(namespace).Get(name, opts) + if err != nil { + return "", pkgerrors.Wrap(err, "Get Deployment error") + } + + return deployment.Name, nil +} diff --git a/src/k8splugin/plugins/namespace/plugin.go b/src/k8splugin/plugins/namespace/plugin.go new file mode 100644 index 00000000..986de863 --- /dev/null +++ b/src/k8splugin/plugins/namespace/plugin.go @@ -0,0 +1,68 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 ( + pkgerrors "github.com/pkg/errors" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// CreateResource is used to create a new Namespace +func CreateResource(namespace string, client *kubernetes.Clientset) error { + namespaceStruct := &coreV1.Namespace{ + ObjectMeta: metaV1.ObjectMeta{ + Name: namespace, + }, + } + _, err := client.CoreV1().Namespaces().Create(namespaceStruct) + if err != nil { + return pkgerrors.Wrap(err, "Create Namespace error") + } + return nil +} + +// GetResource is used to check if a given namespace actually exists in Kubernetes +func GetResource(namespace string, client *kubernetes.Clientset) (bool, error) { + opts := metaV1.ListOptions{} + + namespaceList, err := client.CoreV1().Namespaces().List(opts) + if err != nil { + return false, pkgerrors.Wrap(err, "Get Namespace list error") + } + + for _, ns := range namespaceList.Items { + if namespace == ns.Name { + return true, nil + } + } + + return false, nil +} + +// DeleteResource is used to delete a namespace +func DeleteResource(namespace string, client *kubernetes.Clientset) error { + deletePolicy := metaV1.DeletePropagationForeground + + err := client.CoreV1().Namespaces().Delete(namespace, &metaV1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + + if err != nil { + return pkgerrors.Wrap(err, "Delete Namespace error") + } + return nil +} diff --git a/src/k8splugin/plugins/service/plugin.go b/src/k8splugin/plugins/service/plugin.go new file mode 100644 index 00000000..36ef24f6 --- /dev/null +++ b/src/k8splugin/plugins/service/plugin.go @@ -0,0 +1,131 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +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 ( + "io/ioutil" + "log" + "os" + + "k8s.io/client-go/kubernetes" + + pkgerrors "github.com/pkg/errors" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + + "k8splugin/krd" +) + +// CreateResource object in a specific Kubernetes Deployment +func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { + if kubedata.Namespace == "" { + kubedata.Namespace = "default" + } + + if _, err := os.Stat(kubedata.YamlFilePath); err != nil { + return "", pkgerrors.New("File " + kubedata.YamlFilePath + " not found") + } + + log.Println("Reading service YAML") + rawBytes, err := ioutil.ReadFile(kubedata.YamlFilePath) + if err != nil { + return "", pkgerrors.Wrap(err, "Service YAML file read error") + } + + log.Println("Decoding service YAML") + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode(rawBytes, nil, nil) + if err != nil { + return "", pkgerrors.Wrap(err, "Deserialize service error") + } + + switch o := obj.(type) { + case *coreV1.Service: + kubedata.ServiceData = o + default: + return "", pkgerrors.New(kubedata.YamlFilePath + " contains another resource different than Service") + } + + kubedata.ServiceData.Namespace = kubedata.Namespace + kubedata.ServiceData.Name = kubedata.InternalVNFID + "-" + kubedata.ServiceData.Name + + result, err := kubeclient.CoreV1().Services(kubedata.Namespace).Create(kubedata.ServiceData) + if err != nil { + return "", pkgerrors.Wrap(err, "Create Service error") + } + return result.GetObjectMeta().GetName(), nil +} + +// ListResources of existing deployments hosted in a specific Kubernetes Deployment +func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { + if namespace == "" { + namespace = "default" + } + opts := metaV1.ListOptions{ + Limit: limit, + } + opts.APIVersion = "apps/v1" + opts.Kind = "Service" + + list, err := kubeclient.CoreV1().Services(namespace).List(opts) + if err != nil { + return nil, pkgerrors.Wrap(err, "Get Service list error") + } + result := make([]string, 0, limit) + if list != nil { + for _, service := range list.Items { + result = append(result, service.Name) + } + } + return &result, nil +} + +// DeleteResource deletes an existing Kubernetes service +func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { + if namespace == "" { + namespace = "default" + } + + log.Println("Deleting service: " + name) + + deletePolicy := metaV1.DeletePropagationForeground + err := kubeclient.CoreV1().Services(namespace).Delete(name, &metaV1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + if err != nil { + return pkgerrors.Wrap(err, "Delete Service error") + } + + return nil +} + +// GetResource existing service hosting in a specific Kubernetes Service +func GetResource(name string, namespace string, kubeclient *kubernetes.Clientset) (string, error) { + if namespace == "" { + namespace = "default" + } + + opts := metaV1.GetOptions{} + opts.APIVersion = "apps/v1" + opts.Kind = "Service" + + service, err := kubeclient.CoreV1().Services(namespace).Get(name, opts) + if err != nil { + return "", pkgerrors.Wrap(err, "Get Deployment error") + } + + return service.Name, nil +} -- 2.16.6